Making an AWS static website EVEN MORE secure

OK, so we have a secure website, hosted on Amazon S3, and served up via HTTPS by CloudFront with an Amazon SSL Certificate. But, as we know from last time, we also have to express this security through our response headers. It was fairly easy with Azure – after all, it’s “just” IIS back there, and web.config is the answer to everything once you know the magic incantations – but how to do the same thing on AWS?

For this one, I am indebted to an official Amazon blog post: Adding HTTP Security Headers Using Lambda@Edge and Amazon CloudFront. It’s only 4 months old as well, so this is cutting edge stuff. And, I’d have to say, it’s wacky; it took me several goes to get it right.

First though, please go read or at least skim through that post. It explains what’s going to happen pretty thoroughly and why.

Create a Lambda function

Quoting Amazon’s blurb, AWS Lambda lets you run code in the cloud without provisioning or managing servers. Just write it and upload it to Lambda. Sounds OK, and is a whole new kettle of fish, but here we’re just going to use Lambda@Edge. This is a way to run some code every time a trigger is fired from CloudFront. At the edge of the cloud, in other words. The number of triggers is few and we’ll only be interested in one of them: the trigger that fires when CloudFront gets a response packet from the S3 hosting. In essence, what’s going to happen is that a browser asks for our page, CloudFront asks the hosting for the file, the file returns to CloudFront ready to be passed onto the viewer when, boom, the trigger is fired.

Let’s follow that walkthrough. We go to the Lambda dashboard and click on Create function. As it says, we’re going to Author from scratch. Name the new function (I chose addSecurityHeadersToWITJ). Leave the Runtime option alone. For the Role we’re going to Create new role from Template(s). Name it, and then with the Policy templates dropdown, choose Basic Edge Lambda permissions. Then click the Create function button.

Write the code

We now have what seems to me to be a confusing screen. The initial panel is Designer. Just ignore that for now, and instead scroll down to the Function code panel, where there’s a code editor. Replace the code in that index.js file with this:

'use strict';
exports.handler = (event, context, callback) => {
    
    //Get contents of response
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    //Set new 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'; img-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; object-src 'none'"}]; 
    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'}]; 
    
    //Return modified response
    callback(null, response);
};

As the code indicates, all it does is to retrieve the current response headers, adds the new security-based ones, and then returns the updated response (through a callback). The headers should be familiar from last time, or from the blurb on the securityheaders.com site.

Click the Save button in the top right.

Add the trigger

Now it gets complicated. Well, more weird than complicated. Although we have saved the code, before we can add the trigger, we actually have to publish the new function first, select the new version, and then add the trigger.

So, from the Actions dropdown at the top of the window, select Publish new version. Give it a description (“First version” perhaps), then hit Publish.

Now we can add the trigger. Click on CloudFront in the Add triggers list on the left. The panel underneath changes to Configure triggers. Select your new Distribution from the dropdown, leave the Cache behavior as ‘*’. Set the CloudFront event to Origin response. Check the Enable trigger and replicate box, like it asks you to. Finally click the Add button.

OK, that’s added the trigger, but now you have to save the whole function configuration, so click Save in the upper right.

Wait, wait, and wait some more

Now that we’ve created the function, added the code and the trigger, and pushed it onto our CloudFront distribution, we have to wait for it to be deployed. Go back to your CloudFront dashboard and wait for the deployment-in-progress animation to finish. A good half hour, as I said last time, so you’re just hoping you got everything right first time. (Hint: I didn’t and it was a right royal pain in the **** to fix it all.)

It doesn’t work!

And then you go to securityheaders.com and your domain is scored as an F. Wut? All that work and palaver and waiting, and the security headers are not coming through? What went wrong?

The answer, as I alluded to way back in the first part of this blog series, is that one of CloudFront’s jobs is caching. In essence, CloudFront is not only caching the content but the headers as well. The default that we selected when creating the distribution was Use Origin Cache Headers. But we never set up our S3 bucket to serve up these headers, so CloudFront gets the files the first time they’re requested and then stores them in (and serves them from) its cache. Response headers and all. Forevermore.

What we have to do is invalidate CloudFront’s cache. Go to your CloudFront Distributions dashboard, select your distribution. One of the tabs on the resulting page is Invalidations. Select it.

Click Create Invalidation. The window that’s shown allows you to specify the Object Paths to invalidate. Me, the first time I tried this, entered /*.* to mean “invalidate everything”. You know, because I’m a DOS/Windows guy. Bzzzt, thanks for playing. It took me a couple of goes (and lots of wasted time) before I realized that it should just be /* with nothing else.

Click on Invalidate and wait. Once it’s done, your site should serve up the security headers just fine and you get an A+ from the security headers testing site.

(Aside: Of course you should invalidate the CloudFront cache every time you make a change to the static site otherwise you won’t see your changes in the browser. Another tip I learned through trial and error.)

Locks on Bridge - banner

Loading similar posts...   Loading links to posts on similar topics...

No Responses

Feel free to add a comment...

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response