Introduction#
- Deploy two static web with CloudFront and S3
- Add WAF to protect the web
- Walk through parameters and dicussion
Part 1. Deploy Static Web#
What uses cases?
- Edges location cache content to coler users
- Protect the site with WAF
- Header security - XSS attacks
What is the best practice?
- Pricing
- Cache expire time
- Deployment time
- S3 policy to grant access to CloudFront OAI
- Monitor, log, optimization
What termilogy? - Origin - HTTP header - Distribution
- Create s3 bucket to store static content
const bucket = new aws_s3.Bucket(this, 'BucketHostStaticWeb', {bucketName: 'bucket-static-web',// not production recommendedremovalPolicy: RemovalPolicy.DESTROY,// not production recommendedautoDeleteObjects: true,// block public readpublicReadAccess: false,// block public access - production recommendedblockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL})
- Create CloudFront OAI (identity)
const cloudfrontOAI = new aws_cloudfront.OriginAccessIdentity(this,'CloudFrontOAIIcaDemo',{comment: 'OAI for ICA demo'})
- 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)]}))
- 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,},],});
- 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?
- Protect a web by Geo restriction
- Protect a web from anomaly IP requests
- Protect a web from bad IPs
- Cross-site scripting, SQL injection, ...
WAF Rules
- 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]})
- 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'}}
- 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'}}
- 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'}}
- Attach the web by sending concurrent requests
from calendar import cimport timefrom pymysql import NUMBERimport requestsfrom concurrent.futures import ThreadPoolExecutorNUM_CONCURRENT_REQUEST = 200URL_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 = 1while True:print("send bucket {0} with {1}".format(bucket_count, NUM_CONCURRENT_REQUEST))send_concurrent_request(NUM_CONCURRENT_REQUEST)bucket_count += 1time.sleep(5)
Part 3. Lambda@Edge#
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",});
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 jsonCONTENT = """<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
- config.json
- other solutions
import { CognitoJwtVerifier } from "aws-jwt-verify";import { config } from "./config.js";// verifierconst 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 headerif (!headers.authorization) {console.log("no auth headers");return response;}// try to decode tokenvar 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