Scoring A+ for Security Headers on My Cloudfront-Hosted Static Website

Secure like this castle

On Saturday, I posted my guide on Scoring A+ for Security Headers in Django, following my talk at DjangoCon Europe. I thought it would be a good idea to step up and make my own site score A+, rather than a dismal F! My site isn’t built in Django, but as a Jekyll static site. It’s hosted on AWS S3 and CloudFront.

Unfortunately, CloudFront isn’t so fully featured as other CDN’s and lacks a simple “add headers” configuration. However, its Lambda@Edge feature allows running JavaScript on responses. With this, we can add headers.

I found Tom Cook’s guide on Medium, which got me most of the way. Rather than using the AWS web console, I deploy my site with AWS’ Infrastructure-as-Code tool CloudFormation. Thus I needed to translate the guide into some CloudFormation template changes.

First I added an IAM role for my Lambda function and gave it permission to write logs:

  Type: AWS::IAM::Role
      Version: 2012-10-17
      - Effect: Allow
        Action: sts:AssumeRole

  Type: AWS::IAM::Policy
    - !Ref LambdaIAMRole
    PolicyName: AllowLogging
      Version: 2012-10-17
      - Effect: Allow
        - logs:CreateLogGroup
        - logs:CreateLogStream
        - logs:PutLogEvents
        - !Sub arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunction}
        - !Sub arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunction}:*

Then I created the function, including the code inline in my template so I don’t have to upload it to an S3 bucket. Thanks to Tom Cook for the code, which I tweaked for my headers. I rolled out the headers iteratively, but the final version looks like:

  Type: AWS::Lambda::Function
      ZipFile: |
        'use strict';
        exports.handler = (event, context, callback) => {
            const response = event.Records[0].cf.response;
            const headers = response.headers;

            headers['strict-transport-security'] = [{
              key: 'Strict-Transport-Security',
              value: 'max-age=31536000; includeSubdomains; preload'

            headers['content-security-policy'] = [{
              key: 'Content-Security-Policy',
              value: "default-src 'none'; font-src; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"

            headers['x-content-type-options'] = [{
              key: 'X-Content-Type-Options',
              value: 'nosniff'

            headers['x-frame-options'] = [{
              key: 'X-Frame-Options',
              value: 'DENY'

            headers['x-xss-protection'] = [{
              key: 'X-XSS-Protection',
              value: '1; mode=block'

            headers['referrer-policy'] = [{
              key: 'Referrer-Policy',
              value: 'same-origin'

            callback(null, response);
    Handler: index.handler
    MemorySize: 128
    Role: !GetAtt LambdaIAMRole.Arn
    Runtime: nodejs8.10
    Timeout: 1

After this I had to tackle the complexity of Lambda versioning. CloudFront’s Lambda@Edge configuration requires a specific version of the Lambda function. This is a bit hard to configure with CloudFormation.

With the default behaviour updating to create a new version would mean deleting the old one. The easiest fix to this is to create the version with CloudFormation’s DeletionPolicy: Retain. This tells CloudFormation to retain old instances of the resource instead of deleting them.

Another downside is that if I deploy a new version of the code, I need to rename the version resource in the template to creates a new one.

So the version looks like this in my template (note it’s my eigth iteration here, so I suffixed it ‘8’):

  Type: AWS::Lambda::Version
  DeletionPolicy: Retain
    FunctionName: !GetAtt LambdaFunction.Arn

Then I needed to point my CloudFront distribution to execute the Lambda function version. There are multiple events one can hook into, here I needed the ‘viewer response’ event.

Configuring CloudFront with CloudFormation is my least favourite time so use AWS. It’s hard to get right first time and updating is super slow so testing can take hours. (I wrote most of this blog post in the time it took to update and iterate to A+.)

I added function in the LambdaFunctionAssociations key under the DefaultCacheBehavior. My full CloudFront configuration looks like this :

  Type: AWS::CloudFront::Distribution
        - HEAD
        - GET
        Compress: true
        DefaultTTL: 60
          QueryString: false
        - EventType: viewer-response
          LambdaFunctionARN: !Ref LambdaFunctionVersion8
        MaxTTL: 31536000
        MinTTL: 0
        TargetOriginId: S3-adamj-eu
        ViewerProtocolPolicy: redirect-to-https
    # ...

But now I have my A+ rating!

A+ rating for

(No, I didn’t bother adding Feature-Policy, since it’s still experimental and I haven’t got many scripts.)

Hope this helps you configure your site,


If your Django project’s long test runs bore you, I wrote a book that can help.

Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: , , ,