Thursday, November 09, 2017

The 6 Step Happy Path to HTTPS on Amazon S3 and Cloudfront

Troy Hunt called it back in July, and now the HTTPS tipping point is here. From Chrome 62 onwards, sites are going to be flagged as dangerous if they don't have strong security in place. 

If you haven't already, read his article now - I'll wait.

Of course it doesn't have to be hard to implement - Troy has himself blogged on the "The 6-Step 'Happy Path' to HTTPS" - but I'm hosting my websites out of Amazon AWS, so my "6-step 'Happy Path' to HTTPS on Amazon S3 and Cloudfront" is a little different.

Step 1 - Get a free certificate

Difficulty level: Easy

So whilst I can't use LetsEncrypt, Amazon gives us the tools to add a custom SSL certificate to my Cloudfront distribution.

Go to the AWS Console and the Cloudfront Management module.

Select the distribution for your website, and click on "Edit" on the General tab. Amongst all the setting, you get the options shown right for selecting what certificate to use.

We want a custom SSL certificate, and all we have to do is click on "Request or Import a Certificate with ACM" to start the request process in a new window / tab
NOTE: You must have your AWS console configured for the N. Virginia region when going through the certificate request process. Whilst this should happen automatically, it didn't always for me. YMMV.
Working through the ACM wizard to get a certificate is simple enough that I'll not detail it here - but remember to add both the entry and the * wildcard if you've got sub-domains.

When your cert has been created and validated, go back to the Cloudfront distribution page and hit the refresh button beside the certificate drop-down. Your shiny new certificate should be shown, so select it and save changes.

When the distribution has updated, you'll now be able to access your website using https and the ACM certificate.

Step 2 - Add a 301 "Permanent Redirect"

Difficulty level: Easy

This step is all about telling browsers to always use HTTPS - and Cloudfront has you covered here too.

Select the distribution for your website in the Cloudfront Management module again and this time choose the "Behaviours" tab. I had only a single default behavior, you may have more - if so, then you'll need to make the following change for each.

Check the checkbox and click on the "Edit" button to edit the behaviour. 

The setting we're interested in is "Viewer Protocol Policy" (shown right). 

Set this to "Redirect HTTP to HTTPS" and click on the save button (which is helpfully labelled "Yes, Edit") at the bottom - when the distribution finishes updating your website will now redirect HTTP requests to HTTPS.

Step 3 - Add HSTS

Difficulty level: Medium-Hard

This step is actually the meat of this blog post. Serving your website out of S3 and CloudFront may be cheap, but you don't get all the self-serve features offered by CloudFlare for adding standard security headers.

But all is not lost - we can use AWS Lambda to post-process all responses as they leave CloudFlare.

First, open the Lambda Management module (ensuring you're in the N. Virginia region).

We need to create a new function for each website you're serving from S3 / CloudFront - I've got 3 websites, and have completed this exercise on 2 so far as you can see right.

Click on the "Create Function" button and you'll be presented with a "Blueprints" page shown right. 

We want the cloudfront-modify-response-header blueprint, so click on the title of that card.

Now we're going to have to add some information about the Lambda function before we can create it. Interestingly, we're not actually able to edit the code for the function until after it's been created - we have to take the boiler-plate code as is for now.

Enter a name for your function - remembering that you'll create a new function within your account for each website you host. Something like AddSecurityHeadersForMyDomain might be a good choice here.

If you've never created a Lambda, you'll need to create a role, so select "Create New Role from Template", give it a name and choose "Basic Edge Lambda permissions" as the policy template.

Once you've done that, you can select "Choose an existing role" and pick the role you previously completed - roles can be shared across Lambda functions.

Next, we need to configure how the Lambda links to CloudFront.

Critical here is to select the correct CloudFront distribution - which is of course just a nice long code string. (sigh)

Leave the "Cache Behavior" option set to "*" (the default), and for "CloudFront Event" select "Viewer Response".

You have to check the "Enable trigger and replicate" option at this point to proceed - even though the "Create Function" (Save) button is way down the page below the boilerplate code. Click on that and you've successfully created your Lambda and bound it to your CloudFront distribution.

But, of course, we've yet to actually edit the code for this function to do what we want - namely add the HSTS header.

Click on the "Configuration" tab and you can see the boilerplate code. Helpfully AWS tells us that we can't edit the V1 function we just created, but have to switch to $LATEST - Lambda functions are versioned.

Let's pause though to have a look at what the boilerplate function is doing before we change it.

The function modifies the outbound headers - it takes the value from the "X-Amz-Meta-Last-Modified" header set by S3 as the origin and pastes it into the more standard "Last-Modified" header. 

It's all fairly obvious Node.js stuff, so let's add the HSTS header. Click on "Click here to go to $LATEST" and you'll be presented with an editable code pane.

The code we want to add is almost trivial, and we need to add it just above the callback(null, response) line:

    const hstsName = 'Strict-Transport-Security';
    const hstsValue = 'max-age=31536000; includeSubDomains';

    headers[hstsName.toLowerCase()] = [{
        key: hstsName.toLowerCase(),
        value: hstsValue,


Click on "Save" (in the activity bar at the top of the page) to save the Lambda.

AWS Lambdas have an in-built test harness, so we should configure this - but it's not automated or obvious.

Click on the "Select a test event..." dropdown and click on "Configure test events" to bring up the Create / Edit dialog.
Give your test a name (you can have 10 per function) and click on "Create" to save the test.

Now click on "Save and test" and your Lambda function is run - you should get a "success" banner to say all's well.

Expanding the details section lets you see the input and output of the function - and scrolling down the output area we see our HSTS header has been correctly added.

We're nearly there, honestly.

We have to publish and re-bind the function for it to take effect on our CloudFront distribution. Click on the "Actions" drop-down and click on "Publish new version". 

Enter a descriptive name for this version and click on "Publish".

You'll now be back to the Function details page, but with V2 selected. Click on the "Triggers" tab - and there's nothing there! Our new version needs to be bound to CloudFront, replacing the obsolete V1 version.

Click on "+ Add Trigger" and you get the trigger dialog. This should be pre-populated from the V1 settings so all you have to do is click on "Submit" to rebind to the V2 function. 

Load your site in a browse (you may need a hard-refresh) after a couple of seconds and using the developer tools you should be able see the HSTS header has been added.

Step 3 completed - finally.

Step 4 - Change insecure scheme references

Difficulty level: Boring

Yes, it's boring - but also very easy - to go through your website looking for insecure scheme references.

Most of mine were relative references anyway, so it was only the few external ones that caused any issues - on the home page specifically my LinkedIn badge GIF.

Now you could, of course, use the Lambda we created in Step 3 to replace any 'http://' found in the response body with 'https://' to get the same effect as flicking the switch in CloudFlare does, but for my noddy sites that's overkill.

A quick check using Chrome DevTools very quickly digs out the references - the Security tab is your friend here.

Actually getting the CloudFront distribution pushed so that the latest build of the codebase was being served was more problematic than anything else. Go figure.

Step 5 - Add the "upgrade-insecure-requests" CSP

Difficulty level: Easy

This step is actually easier in S3 and Cloudfront than in CloudFlare, in my opinion.

Now we've got a Lambda that modifies headers, all we need to do is add a couple more lines to add the CSP header:

    const cspName = 'Content-Security-Policy';
    const cspValue = 'upgrade-insecure-requests';

    headers[cspName.toLowerCase()] = [{
        key: cspName.toLowerCase(),
        value: cspValue,


Of course, we have to go round the loop of creating a new version of the function and re-binding it, but I'll leave that to you as an exercise.

Step 6 - Monitor CSP reports

Difficulty level: Trivial

Things have progressed since Troy wrote his article - he's recently joined Scott Helme as a partner in Report-Uri to build out that service.

So all we do for this step is sign up for the Report-Uri service and get a reporting URL from there. Implementing monitoring is then another simple change to our Lambda to add another header:

    const csprName = 'Content-Security-Policy-Report-Only';
    const csprValue = 'default-src https:;report-uri';

    headers[csprName.toLowerCase()] = [{
        key: csprName.toLowerCase(),
        value: csprValue,


And we're done - that's the 6-step Happy Path to HTTPS on Amazon S3 and Cloudfront.

Of course, you should go further - running your site through Scott Helme's gives a load of advice on headers you can add with your Lambda. My personal site got an 'F' rating before I started this exercise - now it's an 'A'. Win!

So here's the full code for the Lambda that gets me the 'A' rating...


No comments: