I’m adding security headers to my S3 and CloudFront hosted website — using Lambda@Edge. It’s pretty easy, here is how 👇

Table of contents
I learned how to do this from Xavier’s excellent blog. I did some modifications to his functions, but a lot is copy and paste.

New function

In the AWS console; navigate to the Lambda service. Make sure the location is N. Virginia, this is required for Lambda@Edge.

Make a new function:

  • Click Create function, top right
  • Author from scratch
  • Enter a function name
  • Choose Node.js 12.x (latest one supported by Lambda@Edge)
  • Click Create function

We only need one file; index.js, and it looks like;

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers || [];

    // Website should only be access over HTTPS, never over HTTP
    // We also allow browsers to use Google 'preloading' service.
    headers['strict-transport-security'] = [{
        key:   'Strict-Transport-Security',
        value: "max-age=31536000; preload"
    }];

    // Tell the browser that the MIME types that we sent are
    // correct and should not be questioned by the browser.
    // This only applies to scripts and stylesheets.
    headers['x-content-type-options'] = [{
        key:   'X-Content-Type-Options',
        value: "nosniff"
    }];

    // Dont allow the site to be rendered inside an iframe.
    headers['x-frame-options'] = [{
        key:   'X-Frame-Options',
        value: "DENY"
    }];

    // Only send the shortened referrer to a foreign origin,
    // full referrer to a local host.
    headers['referrer-policy'] = [{
        key:   'Referrer-Policy',
        value: "strict-origin-when-cross-origin"
    }];

    // We don't need any features and APIs in the browser.
    headers['permissions-policy'] = [{
        key:   'Permissions-Policy',
        value: "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()"
    }];

    callback(null, response);
};

Permissions

To have permission to deploy the function to the edge, we first need to add some trust relationships:

  • Navigate to Identity and Access Management (IAM)
  • Go to Roles
  • Click the role for the function you just created, it will be named something like function_name-role-xxxxxxxx
  • Go to the Trust relationships tab
  • Click Edit trust relationship
  • Add edgelambda.amazonaws.com to the Service array, making it look like this:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Deploy

Alright — time to deploy! Navigate back to the Lambda function we created earlier.

On the function page;

  • Click Add trigger
  • Choose CloudFront
  • Click Deploy to Lambda@Edge
  • Leave it on Configure new CloudFront trigger
  • Choose your distribution and cache behaviour
  • Set CloudFront event to Origin response
  • Check Confirm deploy to Lambda@Edge

Deployed!

If you now navigate to the CloudFront distribution and behaviour you selected, you should see it under Edge Function Associations.

Why the Origin response event?

From the AWS documentation:

  • The function executes after CloudFront receives a response from the origin and before it caches the object in the response.

So CloudFront will cache the response from S3, with the added headers. Minimizing the cost of Lambda executions.

Redeploy

If you make any changes to the function, it needs to be redeployed to the edge.

You can do that on the function page:

  • Click the Actions button, top right
  • Click Deploy to Lambda@Edge
  • Select Use existing CloudFront trigger on this function
  • Click Deploy

Verify

After deploying it, you can verify that the security headers are being added on Security Headers, a project by Scott Helme.

Here are my results:

Security Headers report summary
Security Headers report summary

I also looked at implementing Content-Security-Policy, but it caused issues with video.js and lightgallery.js, and I haven’t taken the time to sit down and figure it out — yet.