Introduction#

GitHub

  • Deploy two static web with CloudFront and S3
  • Add WAF to protect the web
  • Walk through parameters and dicussion

Part 1. Deploy Static Web#

aws_devops-ica drawio(1)

What uses cases?

What is the best practice?

What termilogy? - Origin - HTTP header - Distribution

  1. Create s3 bucket to store static content
const bucket = new aws_s3.Bucket(this, 'BucketHostStaticWeb', {
bucketName: 'bucket-static-web',
// not production recommended
removalPolicy: RemovalPolicy.DESTROY,
// not production recommended
autoDeleteObjects: true,
// block public read
publicReadAccess: false,
// block public access - production recommended
blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL
})
  1. Create CloudFront OAI (identity)
const cloudfrontOAI = new aws_cloudfront.OriginAccessIdentity(
this,
'CloudFrontOAIIcaDemo',
{
comment: 'OAI for ICA demo'
}
)
  1. Bucket grant access to (only) CloudFront OAI
bucket.addToResourcePolicy(
new aws_iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucket.arnForObjects('*')],
principals: [
new aws_iam.CanonicalUserPrincipal(
cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId
)
]
})
)
  1. Create a CloudFront distribution - S3 origin - OAI permissions
const distribution = new aws_cloudfront.Distribution(
this,
"DistributionVideos",
{
defaultBehavior: {
origin: new aws_cloudfront_origins.S3Origin(bucket, {
originAccessIdentity: oai,
}),
},
defaultRootObject: "index.html",
errorResponses: [
{
httpStatus: 403,
responsePagePath: "/index.html",
ttl: Duration.seconds(300),
responseHttpStatus: 200,
},
],
}
);
  1. Deploy the web (upload static content)
new aws_s3_deployment.BucketDeployment(this, 'DeployWebsite', {
sources: [aws_s3_deployment.Source.asset('./lib/website-dist')],
destinationBucket: bucket,
distribution: distribution
})

Part 2. WAF Rules#

What use cases?

WAF Rules

aws_devops-ica drawio (2)

  1. Create WAF ACL
const webAcl = new aws_wafv2.CfnWebACL(this, 'WafCloudFrontProtectIcaDemo', {
defaultAction: { allow: {} },
scope: 'CLOUDFRONT',
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'waf-cloudfront',
sampledRequestsEnabled: true
},
description: 'WAFv2 ACL for CloudFront',
name: 'WafCloudFrontProtectIcaDemo',
rules: [awsMangedRuleIPReputationList, ruleLimiteRequests100, ruleGeoRestrict]
})
  1. AWSManagedRulesCommonRuleSet block bad IPs
const awsMangedRuleIPReputationList: aws_wafv2.CfnWebACL.RuleProperty = {
name: 'AWSManagedRulesCommonRuleSet',
priority: 10,
statement: {
managedRuleGroupStatement: {
name: 'AWSManagedRulesCommonRuleSet',
vendorName: 'AWS'
}
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSIPReputationList'
}
}
  1. Geo restriction
const ruleGeoRestrict: aws_wafv2.CfnWebACL.RuleProperty = {
name: 'RuleGeoRestrict',
priority: 2,
action: {
block: {}
},
statement: {
geoMatchStatement: {
countryCodes: ['US']
}
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'GeoMatch'
}
}
  1. Block anomaly request by a threshold
const ruleLimiteRequests100: aws_wafv2.CfnWebACL.RuleProperty = {
name: 'LimiteRequests100',
priority: 1,
action: {
block: {}
},
statement: {
rateBasedStatement: {
limit: 100,
aggregateKeyType: 'IP'
}
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'LimitRequests100'
}
}
  1. Attach the web by sending concurrent requests
from calendar import c
import time
from pymysql import NUMBER
import requests
from concurrent.futures import ThreadPoolExecutor
NUM_CONCURRENT_REQUEST = 200
URL_CDK_AMPLIFY = "https://d1ooatqwf6thb8.cloudfront.net/"
URL_WELCOME_HAI = "https://d2mb7sioza8ovy.cloudfront.net/"
def send_one_request(id: int, url=URL_CDK_AMPLIFY):
"""
"""
print("send request {0} to {1}".format(id, url))
requests.get(url=url)
def send_concurrent_request(num_concur_request=100):
"""
"""
with ThreadPoolExecutor(max_workers=num_concur_request) as executor:
for k in range(1, num_concur_request):
executor.submit(send_one_request, k)
if __name__ == "__main__":
bucket_count = 1
while True:
print("send bucket {0} with {1}".format(
bucket_count, NUM_CONCURRENT_REQUEST))
send_concurrent_request(NUM_CONCURRENT_REQUEST)
bucket_count += 1
time.sleep(5)

Part 3. Lambda@Edge#

header

reference here

update cdk stack to associate lambda@edge with distribution behavior. It is noted that

  • it take a while for the lambda@edge to be deploy to POP locations
  • lambda function needs version and deployed to cloudfront (see aws lambda console)
  • if do this mannually, need to create a trigger from lambda console
const distribution = new aws_cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin: this.origin,
},
additionalBehaviors: {
"edge-lambda/*": {
origin: this.origin,
edgeLambdas: [
{
functionVersion: func.currentVersion,
eventType: aws_cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
},
],
},
},
defaultRootObject: "index.html",
});

reference here

we can add lamda@edge with four options

  • viewer request, when cloudfront receives a request from a viewer
  • origin request, before cloudfront forwards a request to the origin
  • origin response, when cloudfront receives a response from the origin
  • viewer response, before cloudfront returns the response to the viewer

example lambda function, more samples

import json
CONTENT = """
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple Lambda@Edge Static Content Response</title>
</head>
<body>
<p>Hello from Lambda@Edge! Hai Tran</p>
</body>
</html>
"""
def handler(event, context):
# Generate HTTP OK response using 200 status code with HTML body.
if (1==2):
response = {
'status': '200',
'statusDescription': 'OK',
'headers': {
'cache-control': [
{
'key': 'Cache-Control',
'value': 'max-age=100'
}
],
"content-type": [
{
'key': 'Content-Type',
'value': 'text/html'
}
]
},
'body': CONTENT
}
else:
response = event['Records'][0]['cf']['request']
return response

if the lambda handler returns a response with status 200 and xxx, then cloudfront return it the the user. In constrat, if the handler return the request, then cloudfront will server the content from the origin, which means authenticated in this case

Validate Token#

Please note that the package size limit of lambda@edge should be smaller than (1MB) for view request. So nodejs is better python here. First create a verfifier using aws-jwt-verify lib.

How to secure the userPoolId and clientId in lambda@edge while it does not support environment variables

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { config } from "./config.js";
// verifier
const verifier = CognitoJwtVerifier.create({
userPoolId: config.userPoolId,
tokenUse: "access",
clientId: config.clientId,
});

then the lambda handler

export const handler = async (event, context) => {
const response = {
status: "200",
statusDescription: "OK",
headers: {
"cache-control": [
{
key: "Cache-Control",
value: "max-age=100",
},
],
"content-type": [
{
key: "Content-Type",
value: "text/html",
},
],
},
body: CONTENT,
};
const cfrequest = event.Records[0].cf.request;
const headers = cfrequest.headers;
// no authorization in header
if (!headers.authorization) {
console.log("no auth headers");
return response;
}
// try to decode token
var token = headers.authorization[0].value.slice(7);
try {
let decoded = await verifier.verify(token);
console.log("decoded access token", decoded);
return cfrequest;
} catch {
console.log("failed to decode the token");
return response;
}
};

it is possible to test by curl

curl -H "Authorization: Bearer $token" $url

Trobleshooting#

  • CloudFront x Origin connection error
  • HTTP 403 Forbidden
    • Server understood the request but it refused authorization
    • From S3 - Access Denied and S3 request ID
    • From CloudFront - generated by CloudFront
    • Geo-restriction
  • Route 53 and a domain from another account
  • Request an ACM certificate to prove you own the domain
  • Configure the CloudFront distribution with the ACM cert and custom domain
  • Go to the other account (Route 53) and create a record CNAME

Some testing

  • check hit cloudfront cache
  • check geo restricted SG
curl https://d32kbvvu3drs8u.cloudfront.net/
  • check rate-based IP block
test/test_waf_rate_base_rule.py