Deploy Cloudfront Functions to add security headers with AWS CDK

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:

  • permissions-policy
  • referrer-policy
  • strict-transport-security
  • x-content-type-options
  • x-frame-options
  • x-xss-protection

Create a new file named headers.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function handler(event) {
    var response = event.response
    var headers = response.headers;

    headers['permissions-policy'] = {
        value: 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
    }
    headers['referrer-policy'] = { value: 'same-origin'}; 
    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload' };
    headers['x-content-type-options'] = { value: 'nosniff' }; 
    headers['x-frame-options'] = { value: 'DENY' }; 
    headers['x-xss-protection'] = { value: '1; mode=block' }; 

    return response;
};

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.

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

Install the dependencies

1
pip3 install -r requirements.txt

Create app.py

1
2
3
4
5
6
7
from aws_cdk import core
from stack import WebsiteStack

app = core.App()
WebsiteStack(app, "website")

app.synth()

Create cdk.json

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

Create stack.py with a basic S3 Bucket

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from aws_cdk import core
from aws_cdk import aws_cloudfront as cloudfront
from aws_cdk import aws_s3 as s3

class WebsiteStack(core.Stack):

    def __init__(self, app: core.App, id: str) -> None:
        super().__init__(app, id)

        bucket = s3.Bucket(
            self,
            "bucket",
            website_index_document="index.html",
            public_read_access=True,
            removal_policy=core.RemovalPolicy.DESTROY,
        )

Add the CloudFront Function + Distribution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from aws_cdk import core
from aws_cdk import aws_cloudfront as cloudfront
from aws_cdk import aws_s3 as s3

class WebsiteStack(core.Stack):

    def __init__(self, app, id):
        super().__init__(app, id)

        bucket = s3.Bucket(
            self,
            "bucket",
            website_index_document="index.html",
            public_read_access=True,
            removal_policy=core.RemovalPolicy.DESTROY,
        )

        security_headers = cloudfront.Function(
            self,
            "security_headers",
            code=cloudfront.FunctionCode.from_file(
                file_path="headers.js",
            ),
        )

        distribution = cloudfront.CloudFrontWebDistribution(
            self,
            "cdn",
            origin_configs=[
                cloudfront.SourceConfiguration(
                    s3_origin_source=cloudfront.S3OriginConfig(
                        s3_bucket_source=bucket,
                    ),
                    behaviors=[
                        cloudfront.Behavior(is_default_behavior=True),
                        cloudfront.Behavior(
                            path_pattern="*",
                            function_associations=[
                                cloudfront.FunctionAssociation(
                                    event_type=cloudfront.FunctionEventType.VIEWER_RESPONSE,
                                    function=security_headers,
                                ),
                            ],
                        ),
                    ],
                )
            ],
        )

        core.CfnOutput(
            self,
            "distribution-domain-name",
            value=distribution.distribution_domain_name,
        )

The directory structure should look like this:

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

Deploy the stack

1
cdk 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.