Using AWS Lambda@Edge to add security headers to a static site

By · · 6 mins read · AWS, Tech

In late 2017 I completed a redesign of this web site. As part of the redesign I went from a self-hosted WordPress installation to a static site generated with Jekyll and hosted on Amazon S3 and CloudFront.

While the web site has been served via CloudFront for almost 5 years now, as I still had control over the origin web server (NGINX) I was able to inject the various security headers that I wished to serve. As the site was now being served directly from S3 I wasn’t surprised when I ran a scan on securityheaders.com and received a nasty big F.

Failed Security Scan

Obviously this wasn’t something that I wanted to continue with and as the site was now entirely serverless, I thought there had to be a better way so I started looking at Lambda@Edge.

AWS Lambda@Edge lets you run Lambda functions to customise the content that CloudFront delivers. Unlike traditional Lambda, Lamba@Edge runs in the CloudFront location closest to the viewer therefore not compromising performance.

The following are the steps I took to solve this issue (note that I now deploy all of this with CloudFormation but that’s out of scope for this post).

Write the Lambda code

I’d already used Lambda for some other functions so was familiar with how to write and deploy the code. Lambda@Edge does however have a few extra restrictions and as of writing only supports Node.js 6.10.

With that in mind, I put together the following code to replicate the headers that I was previously serving from NGINX:

'use strict';

exports.handler = (event, context, callback) => {

    const response = event.Records[0].cf.response;
    const headers = response.headers;

    response.headers['Strict-Transport-Security'] = [{
        key: 'Strict-Transport-Security',
        value: 'max-age=31536000; includeSubDomains; preload',
    }];
    response.headers['X-XSS-Protection'] = [{
        key: 'X-XSS-Protection',
        value: '1; mode=block',
    }];
    response.headers['X-Content-Type-Options'] = [{
        key: 'X-Content-Type-Options',
        value: 'nosniff',
    }];
    response.headers['X-Frame-Options'] = [{
        key: 'X-Frame-Options',
        value: 'SAMEORIGIN',
    }];
    response.headers['Referrer-Policy'] = [{
        key: 'Referrer-Policy',
        value: 'no-referrer-when-downgrade',
    }];
    response.headers['Content-Security-Policy'] = [{
        key: 'Content-Security-Policy',
        value: 'upgrade-insecure-requests;',
    }];
    callback(null, response);

};

Make sure to change the Content-Security-Policy header to suit your requirements. Mine for example is:

default-src 'none'; connect-src 'self' stats.concordant.com.au https://*.algolia.net https://*.algolianet.com shaun.report-uri.com; font-src 'self' assets.shaun.net; frame-src 'self' www.youtube.com twitter.com platform.twitter.com syndication.twitter.com; img-src 'self' assets.shaun.net stats.concordant.com.au syndication.twitter.com platform.twitter.com *.twimg.com data:; script-src 'self' assets.shaun.net stats.concordant.com.au cdnjs.cloudflare.com platform.twitter.com cdn.syndication.twimg.com syndication.twitter.com; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com fonts.googleapis.com platform.twitter.com ton.twimg.com; upgrade-insecure-requests; report-uri https://shaun.report-uri.com/r/d/csp/enforce

Deploy the Lambda function

Just like certificates, Lambda@Edge functions must be deployed in the us-east (N. Virginia) region so you will need to ensure that you have switched to this region before starting.

The first step is to create your Lambda function. In this case we use the Author from scratch setting:

Create Lambda Function

Once you select this preset, you will need to fill out some values to create your function. For example:

Create Lambda Function

The fields are:

  • Name — The name of your function. Try to keep it descriptive.
  • Runtime — This should be Node.js 6.10 to run at edge.
  • Role — This defines what permissions your function should have. If you don’t already have an appropriate role, Lambda provide some templates that you can use to get started quickly.
  • Role name — If you are creating a role choose something that’s descriptive and fits within your naming conventions.
  • Policy templates — This function won’t be interacting with the account, it only needs the Basic Edge Lambda permissions template.

Click Create.

With that done, you need to add your code to the index.js file. You can use the console editor to do this.

Create Lambda Function

Save the code once you are satisfied.

Now you need to publish your function. On the top navigation click Actions → Publish New Version. Enter a description and click Publish.

Create Lambda Function

With a published version, you’ll now have an ARN displayed at the top of the window that points to your published version. This looks something like arn:aws:lambda:us-east-1:123412341234:function:shaun-net-origin-response:1 - — take note of it as you’ll need it for the next step.

Configure CloudFront

To actually get CloudFront to execute the code you need to update your distribution behaviour. To do this, I navigated to my CloudFront distribution and then modified the default behaviour.

The section that we care about is Lambda Function Associations. There are four different types:

  • CloudFront Viewer Request — This function executes when CloudFront receives a request from a viewer, before it checks to see whether the requested object is in the edge cache.
  • CloudFront Viewer Response — This function executes before returning the requested object to the viewer. Like Viewer Request, this function triggers on every request.
  • CloudFront Origin Request — This function executes only when CloudFront forwards a request to your origin. If the object is in the edge cache, it doesn’t execute.
  • CloudFront Origin Response — This function executes after CloudFront receives a response from the origin and before it caches the object in the response. This is the one we want as it ensures the headers will be cached with the content!

Taking note of the ARN of my deployed function, I created an Origin Response function association:

Lambda Function Association

With that done, I clicked the “Yes, Edit” button to save my changes and waited for my Distribution Status to reflect Deployed. It’s time to test.

You’re done

With the code successfully deployed I flushed my CloudFront cache and ran another scan. Much to my delight, I received an A+ response:

Successful Security Scan

Caveats

  • This post is designed as a bit of a quick start and doesn’t necessarily adhere to the usual best practices that I follow when deploying to AWS. I usually only deploy through CloudFormation, but as this is a tutorial I have shown how to create these resources in the console.
  • You should also write tests to ensure that your Lambda@Edge function is working correctly before publishing and deploying the function to your CloudFront distribution.
  • And of course — I’m not responsible for any outages that you may cause!