Production hardening
Hub › AWS › Advanced › Production hardening
Goal
After this page you will have added WAF, rate limiting, CORS hardening, a custom domain with HTTPS, and a multi-region disaster recovery plan to your stack.
Prerequisites
- CloudWatch logs and alarms
- A deployed CDK stack with API Gateway, Lambda, DynamoDB, and CloudFront.
WAF on the API Gateway
AWS WAF (Web Application Firewall) blocks common attack patterns before they reach your Lambda:
# PLACEHOLDER — replace abcd1234 with your API Gateway ID.
WAF_ARN=$(aws wafv2 create-web-acl \
--name items-api-waf \
--scope REGIONAL \
--default-action 'Allow={}' \
--rules '[
{
"Name": "RateLimit",
"Priority": 1,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimitMetric"
}
},
{
"Name": "AWS-AWSManagedRulesCommonRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSCommonRulesMetric"
}
}
]' \
--region us-east-1 \
--query Summary.ARN \
--output text)
aws wafv2 associate-web-acl \
--web-acl-arn $WAF_ARN \
--resource-arn arn:aws:apigateway:us-east-1::/apis/abcd1234The rules above:
- Rate limit — 2,000 requests per 5-minute window per IP.
- AWS managed common rule set — SQL injection, XSS, known exploit patterns.
WAF on CloudFront
CloudFront distributions use CLOUDFRONT scope WAF ACLs instead of REGIONAL:
# PLACEHOLDER — replace EXAMPLE123DIST with your distribution ID.
CF_WAF_ARN=$(aws wafv2 create-web-acl \
--name cloudfront-waf \
--scope CLOUDFRONT \
--default-action 'Allow={}' \
--rules '[
{
"Name": "RateLimit",
"Priority": 1,
"Statement": {
"RateBasedStatement": {
"Limit": 5000,
"AggregateKeyType": "IP"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "CFRateLimit"
}
},
{
"Name": "AWSManagedRulesCommonRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "CFAWSCommon"
}
}
]' \
--query Summary.ARN \
--output text)
aws cloudfront update-distribution \
--id EXAMPLE123DIST \
--web-acl-id $CF_WAF_ARNCORS hardening
Tighten the Lambda CORS header from * to your actual domain:
const ALLOWED_ORIGIN = "https://example.com"; // PLACEHOLDER — your real domain
export const handler = async (event) => {
const origin = event.headers?.origin || "";
const allowedOrigin = origin === ALLOWED_ORIGIN ? ALLOWED_ORIGIN : "null";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": allowedOrigin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
body: JSON.stringify(items),
};
};API Gateway also needs an OPTIONS route for preflight requests. Add it in CDK:
api.addRoutes({
path: "/items",
methods: [HttpMethod.OPTIONS],
integration: new HttpLambdaIntegration("OptionsIntegration", new Function(this, "OptionsHandler", {
runtime: Runtime.NODEJS_20_X,
handler: "index.optionsHandler",
code: Code.fromAsset("./lambda"),
})),
});Custom domain with ACM
A custom domain (e.g. api.example.com) requires an ACM certificate in us-east-1 and a Route 53 A record:
# PLACEHOLDER — replace with your domain.
CERT_ARN=$(aws acm request-certificate \
--domain-name api.example.com \
--validation-method DNS \
--region us-east-1 \
--query CertificateArn \
--output text)
# Add the CNAME to Route 53 for domain validation (output from describe-certificate).
aws acm describe-certificate --certificate-arn $CERT_ARN --region us-east-1After validation, create the API Gateway custom domain:
# PLACEHOLDER — replace with your certificate ARN and domain.
aws apigatewayv2 create-domain-name \
--domain-name api.example.com \
--domain-name-configurations CertificateArn=$CERT_ARN \
--region us-east-1
aws apigatewayv2 create-api-mapping \
--api-id abcd1234 \
--domain-name api.example.com \
--stage '$default'Multi-region disaster recovery
A simple DR plan: replicate DynamoDB to a second region with global tables, and deploy the CDK stack there with a CloudFront origin group as fallback.
Enable DynamoDB global tables:
# PLACEHOLDER — run after creating the table in the second region.
aws dynamodb create-global-table \
--global-table-name items \
--replication-group RegionName=us-east-1 RegionName=us-west-2CDK stack in the second region:
# PLACEHOLDER — use your own account and region.
npx aws-cdk deploy --region us-west-2CloudFront origin group with failover (via the console or CloudFront API):
Origin 1: us-east-1 API Gateway (primary)
Origin 2: us-west-2 API Gateway (failover)
Failover criteria: 5xx, 4xxCheckpoint
Verify WAF is active:
aws wafv2 get-web-acl \
--name items-api-waf \
--scope REGIONAL \
--id $(aws wafv2 list-web-acls --scope REGIONAL --query "WebACLs[?Name=='items-api-waf'].Id" --output text) \
--region us-east-1Then verify the API still responds:
curl -H "Authorization: Bearer eyJ..." https://abcd1234.execute-api.us-east-1.amazonaws.com/itemsThe WAF is transparent to legitimate traffic but drops malformed requests before they reach your Lambda.
What you built
Over 10 pages in this advanced tier you have:
- Replaced hardcoded Lambda data with a DynamoDB table.
- Added Cognito authentication and a Lambda authorizer.
- Codified all infrastructure with CDK.
- Enabled local testing with SAM.
- Learned cross-stack references.
- Set up CI/CD with CodePipeline.
- Added observability with CloudWatch logs, metrics, and alarms.
- Hardened production with WAF, rate limits, CORS, custom domain, and DR.
You have completed the advanced tier.