Serving custom headers from static sites on CloudFront/S3 with Lambda@Edge
[Editor’s Note (12/21/17): This post previously covered the pre-launch configuration for Lambda@Edge. Since launch the formatting changed, and this post has been updated to work with the current version of Lambda@Edge. Thanks to all the commenters that posted clarifications in the interim.]
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, 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:
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.
At re:Invent 2016, AWS introduced Lambda@Edge. This functionality glues Lambda functions into the request/response path for CloudFront distributions at the CDN edge. I strongly recommend watching the re:Invent 2016 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:
- 30 seconds or 5 seconds max runtime, depending on how the function runs
- 128MB max memory
- 50MB or 1MB total function size, depending on how the function runs (including any bundled libraries)
- Node.js 6.10 is the only supported language
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:
Once your lambda function is saved, you’ll need to create a static “version” of it. Do this by clicking “Actions” and “Publish New Version” at the top of the Lambda console. Once published the function ARN at the top will change to include the version number at the end.
The next step is to associate this function with my CloudFront distribution, which is configured as a Lambda trigger. Go into the CloudFront configuration for your distribution, select Behaviors then pick the default behavior out of the list and click Edit. At the bottom of the next page will be a place to add the configuration of your Lambda function. Be sure to paste in the function ARN with the version number and select Viewer Response as the Event Type:
Once you save the changes and CloudFront redeploys the distribution, all new 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. 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.