DEV Community

Cover image for Security Architecture Decisions in My Cloud Resume Challenge
Joshua Michael Hall
Joshua Michael Hall

Posted on

Security Architecture Decisions in My Cloud Resume Challenge

When I deployed my Cloud Resume Challenge project, I made deliberate security decisions at every layer. Some were exactly right. Others were conscious tradeoffs for a portfolio project that I'd handle differently in production.

Here's what I built, why, and what I'd change for enterprise deployment.

The Architecture

Standard Cloud Resume Challenge: S3, CloudFront, Lambda, DynamoDB. I added security controls most candidates skip:

  • Frontend: S3 → CloudFront (OAI, no public bucket)
  • Backend: API Gateway → Lambda → DynamoDB
  • Infrastructure: 100% Terraform, zero console clicking
  • Security: WAF, CloudTrail, GuardDuty, encryption, IAM least privilege

What Worked Well

CloudFront as Security Boundary

My S3 bucket has no public access. All traffic flows through CloudFront via Origin Access Identity:

resource "aws_cloudfront_origin_access_identity" "oai" {
  comment = "OAI for resume website"
}

resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = {
        AWS = aws_cloudfront_origin_access_identity.oai.iam_arn
      }
      Action   = "s3:GetObject"
      Resource = "${aws_s3_bucket.website.arn}/*"
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

This means:

  • No direct bucket enumeration attacks
  • CloudFront handles TLS with ACM certificates
  • WAF rules apply at the edge
  • Defense in depth—every layer enforces policy

WAF with Rate Limiting

resource "aws_wafv2_web_acl" "main" {
  name  = "resume-waf"
  scope = "CLOUDFRONT"

  default_action {
    allow {}
  }

  rule {
    name     = "RateLimit"
    priority = 1

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name               = "RateLimitRule"
    }
  }

  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 2

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name               = "CommonRuleSet"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Overkill for a visitor counter? Probably. But it demonstrates application-layer protection, and the managed rules catch common attacks without maintaining signatures.

IAM Least Privilege

My Lambda has exactly three permissions:

resource "aws_iam_role_policy" "lambda_dynamodb" {
  name = "lambda-dynamodb-policy"
  role = aws_iam_role.lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:UpdateItem"
        ]
        Resource = aws_dynamodb_table.visitor_count.arn
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

No wildcards. No AmazonDynamoDBFullAccess. The table ARN is explicit. Adding a second table means updating the policy—that friction is a feature.

CloudTrail with Integrity Validation

resource "aws_cloudtrail" "main" {
  name                          = "resume-trail"
  s3_bucket_name               = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail        = false
  enable_log_file_validation   = true

  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }
}
Enter fullscreen mode Exit fullscreen mode

enable_log_file_validation = true means logs can't be tampered with without detection. When an auditor asks "how do I know these logs weren't modified?", point to the checksum chain.

Conscious Tradeoffs

No VPC

My architecture is purely serverless. Lambda, API Gateway, DynamoDB, S3, CloudFront—none require a VPC.

For a portfolio project, this is fine. For production with sensitive data, I'd want:

  • Lambda in private subnets
  • VPC endpoints for AWS services
  • No public internet from compute layer

Single Region

Everything lives in us-east-1. If I deployed resources elsewhere, I'd have blind spots.

For production:

  • GuardDuty in all regions (detect unauthorized activity anywhere)
  • Multi-region CloudTrail
  • SCPs preventing resource creation in unauthorized regions

No AWS Config

Config tracks configuration drift and evaluates compliance rules. I skipped it because cost/complexity wasn't justified for a portfolio project.

For production, Config is essential: "Show me all S3 buckets allowing public access" becomes a one-click query.

What I'd Add for Production

Service Control Policies

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    },
    {
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

SCPs create guardrails that even admins can't bypass. Deny all actions in non-approved regions. Deny disabling CloudTrail. These prevent entire categories of misconfiguration.

Automated Response

GuardDuty can trigger Lambda to respond automatically:

  • Compromised credentials detected → revoke immediately
  • Unusual API calls from new IP → add to WAF block list

I didn't implement this because complexity wasn't justified. For production, automated response reduces MTTC from hours to seconds.

The IaC Advantage

Everything is in Terraform. Security controls aren't bolt-on afterthoughts—they're part of the infrastructure definition, subject to code review, tracked in Git.

When an auditor asks "show me your WAF configuration," I don't screenshot the console. I show the Terraform file. When they ask "has this changed?", I show the Git history.

This is Configuration Management (CM-2, CM-3, CM-6 in NIST 800-171). It's not compliance theater—it's how good engineers naturally work.

Key Takeaways

  1. Design for audit from the start. Terraform configs, CloudTrail logs, IAM policies—every decision has evidence.

  2. Understand your tradeoffs. No VPC was a conscious choice, not an oversight. Document why, not just what.

  3. Layer your controls. OAI restricts S3. WAF filters requests. IAM limits Lambda. DynamoDB encrypts at rest. No single control is sufficient.

  4. Know when to skip complexity. I didn't implement Security Hub because it wasn't justified for my use case. Knowing when NOT to add controls matters.

  5. Use Infrastructure as Code. Manual configurations drift. Terraform is authoritative.


The full Terraform code is in my GitHub repo. The architecture runs about $15-20/month with all security controls enabled.

Questions about the implementation? Drop a comment or connect with me on LinkedIn.

Top comments (0)