how.wtf

Deploy Cloudfront Functions to add security headers with AWS CDK

· Thomas Taylor

With the newly published CloudFront Functions, developers can leverage fast and short lived functions to handle simplistic tasks for viewer requests and responses.

The AWS article covers the differences between Lambda@Edge and CloudFront Functions in detail.

For a quick reference, here is the table from it:

AttributeCloudFront FunctionsLambda@Edge
Runtime supportJavaScript (ECMAScript 5.1 compliant)Node.js / Python
Execution location218+ CloudFront Edge Locations13 CloudFront Regional Edge Caches
CloudFront triggers supportedViewer request / Viewer responseViewer request /Viewer response / Origin request / Origin response
Maximum execution timeLess than 1 millisecond5 seconds (viewer triggers) 30 seconds (origin triggers)
Maximum memory2MB128MB (viewer triggers) / 10GB (origin triggers)
Total package size10 KB1 MB (viewer triggers) / 50 MB (origin triggers)
Network accessNoYes
File system accessNoYes
Access to the request bodyNoYes
PricingFree tier available; charged per requestNo free tier; charged per request and function duration

The Cloudfront Function Code

Using Javascript (ECMAScript 5.1 compliant), the following code adds common security headers to viewer responses:

Create a new file named headers.js:

 1function handler(event) {
 2    var response = event.response
 3    var headers = response.headers;
 4
 5    headers['permissions-policy'] = {
 6        value: 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
 7    }
 8    headers['referrer-policy'] = { value: 'same-origin'}; 
 9    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload' };
10    headers['x-content-type-options'] = { value: 'nosniff' }; 
11    headers['x-frame-options'] = { value: 'DENY' }; 
12    headers['x-xss-protection'] = { value: '1; mode=block' }; 
13
14    return response;
15};

This code was a modified version from the AWS documentation described here.

The AWS CDK Code

The production version of the code in this section can be found in the how.wtf open source repository.

Create a requirements.txt

For this tutorial, version 1.111.0 of the AWS CDK was used.

1aws-cdk.aws_cloudfront===1.111.0
2aws-cdk.aws_s3===1.111.0
3aws-cdk.core===1.111.0

Install the dependencies

1pip3 install -r requirements.txt

Create app.py

1from aws_cdk import core
2from stack import WebsiteStack
3
4app = core.App()
5WebsiteStack(app, "website")
6
7app.synth()

Create cdk.json

1{
2    "app": "python3 app.py"
3}

Create stack.py with a basic S3 Bucket

 1from aws_cdk import core
 2from aws_cdk import aws_cloudfront as cloudfront
 3from aws_cdk import aws_s3 as s3
 4
 5class WebsiteStack(core.Stack):
 6
 7    def __init__(self, app: core.App, id: str) -> None:
 8        super().__init__(app, id)
 9
10        bucket = s3.Bucket(
11            self,
12            "bucket",
13            website_index_document="index.html",
14            public_read_access=True,
15            removal_policy=core.RemovalPolicy.DESTROY,
16        )

Add the CloudFront Function + Distribution

 1from aws_cdk import core
 2from aws_cdk import aws_cloudfront as cloudfront
 3from aws_cdk import aws_s3 as s3
 4
 5class WebsiteStack(core.Stack):
 6
 7    def __init__(self, app, id):
 8        super().__init__(app, id)
 9
10        bucket = s3.Bucket(
11            self,
12            "bucket",
13            website_index_document="index.html",
14            public_read_access=True,
15            removal_policy=core.RemovalPolicy.DESTROY,
16        )
17
18        security_headers = cloudfront.Function(
19            self,
20            "security_headers",
21            code=cloudfront.FunctionCode.from_file(
22                file_path="headers.js",
23            ),
24        )
25
26        distribution = cloudfront.CloudFrontWebDistribution(
27            self,
28            "cdn",
29            origin_configs=[
30                cloudfront.SourceConfiguration(
31                    s3_origin_source=cloudfront.S3OriginConfig(
32                        s3_bucket_source=bucket,
33                    ),
34                    behaviors=[
35                        cloudfront.Behavior(is_default_behavior=True),
36                        cloudfront.Behavior(
37                            path_pattern="*",
38                            function_associations=[
39                                cloudfront.FunctionAssociation(
40                                    event_type=cloudfront.FunctionEventType.VIEWER_RESPONSE,
41                                    function=security_headers,
42                                ),
43                            ],
44                        ),
45                    ],
46                )
47            ],
48        )
49
50        core.CfnOutput(
51            self,
52            "distribution-domain-name",
53            value=distribution.distribution_domain_name,
54        )

The directory structure should look like this:

project/
├── app.py
├── cdk.json
├── headers.js
├── requirements.txt
└── stack.py

Deploy the stack

1cdk deploy website

Because of the CfnOutput, the distribution’s domain name is exposed via an output on the stack:

Outputs:
website.distributiondomainname = hostname.cloudfront.net

Add index.html document to the S3 bucket

1<h1>Security headers!</h1>

Test the headers

After adding the index.html document, visit the distribution’s domain name to ensure it is working correctly.

To test the security headers, either use your favorite request tool or use securityheaders.com.

#aws   #aws-cdk   #python   #serverless  

Reply to this post by email ↪