Serving custom headers from static sites on CloudFront/S3 with Lambda@Edge

Serving static sites from S3 buckets fronted by CDNs like CloudFront, Fastly, CloudFlare, Akamai, etc has been a common pattern for those that want a quick, secure, and cheap way to get content online.

However, for AWS customers that want to only use S3 and CloudFront there’s been a big limitation: CloudFront does not support adding arbitrary HTTP headers to responses. It allows origin servers to pass through headers, but S3 doesn’t allow adding custom headers to HTTP responses.

Security via HTTP headers

Headers like HSTS, HPKP, CSP, and others provide tangible security benefits for site operators and their users. In particular, HSTS ensures that all requests to a site occur over HTTPS (even more so with browser preloading) which is increasingly important to protect users from ISPs/governments recording or manipulating their private communications.

Since its registration in March 2015, my personal website (tom.horse) has been the most secure .horse domain name on the Internet. Not only is it HSTS preloaded into every browser, but it has an A+ score on Mozilla’s Observatory and SSL Labs. It’s even fully IPv6 compatible since CloudFront added support in October 2016.

The site is a simple static html page, but I want to run it with the absolute minimum of infrastructure and at the cheapest possible cost. In particular, I don’t want any Linux servers in the critical path. Many of its security measures can only be delivered via HTTP headers, so the ability to add custom headers is crucial to maintaining its A+ rating.

The story until recently

The common solution (besides using a different CDN) has been to serve static sites via an origin server that could add the additional headers to requests, such as nginx:

nginx config snippet: Proxying an S3 bucket and adding various HTTP headers in nginx. This is pretty dumb.

This requires an always-on EC2 host to function as an origin server. Since the whole point of the S3/CloudFront sandwich is to not run servers, this is less than ideal.

Enter Lambda@Edge

At re:Invent 2016, AWS introduced Lambda@Edge. This is new functionality that glues Lambda functions into the request/response path for CloudFront distributions at the CDN edge. This functionality is currently in beta, and will launch sometime in the future. I strongly recommend watching the re:Invent session video for much more detail.

At the moment Lambda@Edge functions are extremely limited, as the CloudFront PoPs don’t have the computational capacity of larger AWS regions, and functions shouldn’t be allowed to interrupt the request flow for too long. In particular:

  • 50ms max runtime
  • 128MB max memory
  • 1MB total function size (including any bundled libraries)
  • Node.js 4.3 is the only supported language
  • No native access to “other” resources like DynamoDB, S3, network calls, etc.

These restrictions are fine for now; all we’re doing is adding additional HTTP headers to requests before they’re returned to clients. The above nginx header config becomes the following lambda function:

The next step is to associate this function with my CloudFront distribution, which is configured as a Lambda trigger. Since these headers need to be added after the S3 origin responds, the event type is “viewer-response”:

Once this is added as a trigger, my S3 bucket is set as the origin, and CloudFront redeploys the distribution, all headers are returned as expected:

Now I can turn off nginx on the host previously responsible for adding the headers, or even shut it down to save money. Per the Lambda pricing structure (requests/mo + execution duration) even a few million requests per month should only cost a couple extra bucks.

This is an extremely simple, static example of Lambda@Edge’s functionality in beta. Once it’s generally available I look forward to seeing clever uses for running custom code on the CDN edge, as well as AWS lifting some of the initial restrictions to enable more use cases.

Further Resources