Securing an S3 bucket with Cloudfront and a Lambda

Introduction

(Note: This is an old article. The approach will still work, but nowadays I’d advise doing this with Terraform or Pulumi)

Recently at work, I needed to secure a component library we had built. It was displayed with Storybook, and deployed as a static site to an S3 bucket. This works really well, but we want to keep this collection of components internal to the company.

I had tried a few different ways of restricting access, but most were quite frustrating. For example, I could use the bucket policy to block everyone other than an allowed list of IPs which belonged to my colleagues. This doesn’t scale very well though, since IP addresses frequently change, people travel, etc. I didn’t want to keep needing to maintain a bucket policy full of random addresses.

The solution we ended up coming to was to put the S3 bucket behind an AWS Cloudfront distribution (effectively an Amazon CDN) and use a Lambda (a handy serverless function) to handle the auth.

Here’s how to do it

Set up your bucket

Firstly you want your bucket with whatever it is you’re providing access to. Ensure in your bucket properties, you set ‘Static web hosting’ to be available.

You can have your public access settings on or off since they will be superseded by our later changes, but I always default to removing public access, just in case.

Set up CloudFront

Next up, we’ll want to get CloudFront running. Create a new distribution and set the Origin Domain Path to your S3 bucket. I then enable GET, HEAD, OPTIONS as the allowed HTTP methods, and in the case of my work, pass in a custom SSL certificate matched to the domain we are using.

Once you’ve filled in your options, go ahead and create the distribution.

Now, go to the Distribution’s settings and check the ‘Origin and Origin Groups’. There should only be one Origin at this point which you can edit. Also, ensure you set your Default Root Object as ‘index.html’ since we’re pointing to a static website in our case.

Select restrict bucket access and then create a new identity. The field should be automatically populated for you. Then check Yes, Update bucket policy so these changes are reflected in S3. Save your changes. The form should look similar to the image below:

'Screenshot of CloudFront origin policy settings'

Add your Lambda

Now we’re accessing our bucket via CloudFront, we can secure it with a Lambda! This is super easy, just go to Lambdas, create a new function, and author it from scratch with the latest Node runtime.

In your function code, add the following:

"use strict"
exports.handler = (event, context, callback) => {
  // Get request and request headers
  const request = event.Records[0].cf.request
  const headers = request.headers

  // Configure authentication
  const authUser = "user"
  const authPass = "pass"

  // Construct the Basic Auth string
  const authString =
    "Basic " + new Buffer(authUser + ":" + authPass).toString("base64")

  // Require Basic authentication
  if (
    typeof headers.authorization == "undefined" ||
    headers.authorization[0].value != authString
  ) {
    const body = "Unauthorized"
    const response = {
      status: "401",
      statusDescription: "Unauthorized",
      body: body,
      headers: {
        "www-authenticate": [{ key: "WWW-Authenticate", value: "Basic" }],
      },
    }
    callback(null, response)
  }

  // Continue request processing if authentication passed
  callback(null, request)
}

Thanks to lmakarov

The credentials are hardcoded in here. Adjust as you see fit. Lambdas also support network calls for viewer request/response events, so you can support checking credentials against an external source.

Now add a trigger to this code and select ‘CloudFront’ as per the image below:

'Screenshot of Lambda Triggers'

You will then need to deploy this to ‘Lambda@Edge’. From here, make sure you have the correct distribution ID in the field, and set the CloudFront Event to ‘Viewer request’.

Wait for this to propoagate via CloudFront.

Done!

That’s it! You now have a secured S3 bucket which is accessed via CloudFront. Also worth noting that users cannot sneak around CloudFront if they know the S3 direct URL. Access gets restricted to CloudFront exclusively so you have to go through that, and the authentication we have put in place.

Found this useful? Tweet me @ruairidhwm and let me know!

A tech newsletter that teaches you something new

This blog was created to document my own learning, and share useful tips with other software engineers.

My newsletter is like that, but straight to your inbox! It contains useful links I've found around the web, sneak peeks of my new articles, and access to free resources I've created.

Sign Up

You can unsubscribe whenever you'd like, and I probably hate spam even more than you do.