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}/*"
}]
})
}
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"
}
}
}
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:*:*:*"
}
]
})
}
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
}
}
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": "*"
}
]
}
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
Design for audit from the start. Terraform configs, CloudTrail logs, IAM policies—every decision has evidence.
Understand your tradeoffs. No VPC was a conscious choice, not an oversight. Document why, not just what.
Layer your controls. OAI restricts S3. WAF filters requests. IAM limits Lambda. DynamoDB encrypts at rest. No single control is sufficient.
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.
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)