During the deployment of our frontend to CloudFront we encountered the problem of not being able to configure the HTTP Security Headers, which is an essential configuration for reducing the attack surface of web applications. We resolved this issue by using Amazon’s new Lambda@Edge functions to attach the headers before the response is sent to the clients.
What are HTTP Security Headers and why should I use them?
If you request a website from a web server, the server does not only respond with the content (e.g. HTML or image) but also with a variety of HTTP Headers. These headers contain information like the content type of the response or the version of the web server. A special category of headers are the HTTP Security Headers, which are attached to the web server’s response to enforce a certain set of security rules in the client’s browser.
The configuration of the security headers is an essential step of securing your web application against certain types of attacks and helps to reduce the attack surface of your application. Attacks like Click-Jacking, Cross-Site-Scripting (XSS) and Cross-Site-Request-Forgery (CSRF) can be effectively mitigated by the right configuration of HTTP Security Headers. For more details about the different HTTP Security Headers please refer to our wiki.
How to set HTTP Security Headers in CloudFront?
As mentioned before, we are relying on the content delivery network (CDN) CloudFront to host our frontend, which is located in a S3-Bucket. This enables the commonly known advantages of a heavily reduced load time and latency through the distribution to a variety of local caching servers. However as a security company we are not compromising on the security of our applications and required the ability to follow best practices like the configuration of the HTTP Security Headers.
Unfortunately, neither CloudFront nor S3 supports the configuration of HTTP Security Headers out of the box. There is the possibility to setup Cross-origin resource sharing (CORS) in S3, but this does not suffice to implement all best practices. Luckily Amazon released the relatively new Lambda@Edge, which allows to implement a (more or less) easy solution.
Lambda@Edge allows the implementation of Lambda functions, which are distributed to the caching servers of CloudFront and executed at “the edge” (= caching servers). The execution of the function could be triggered by four different events (see Lambda@Edge Documentation):
- Viewer request: After CloudFront receives a request from a viewer.
- Origin request: Before CloudFront forwards the request to the origin (in our case the S3-Bucket).
- Origin response: After CloudFront receives the response from the origin (in our case the S3-Bucket).
- Viewer response: Before CloudFront forwards the response to the viewer.
The last event type is the one we need to attach the HTTP Security Headers. Whenever a viewer requests a file from CloudFront it locates the file in the local cache or fetches it from the S3-Bucket, which is our origin. Then Lambda@Edge intercepts the response before it is forwarded to the viewer and executes our Lambda function, which attaches the HTTP Security Headers. Finally the altered response is forwarded to the viewer.
How to configure it?
Warning: Please be aware that an error in the Lambda function, will cause your CloudFront distribution to crash and become unavailable! Do not use your production CloudFront distribution for the initial setup and testing.
Step 1: Create the Lambda function
- Open the AWS console and select the us-east-1 region.
- Navigate to Lambda in the AWS console.
- Click on Create Function and choose the cloudfront-modify-response-header blueprint.
Step 2: Configure the CloudFront trigger
- Select the appropriate Distribution ID for your CloudFront distribution.
- Select the CloudFront Event to Viewer Response.
- Check Enable trigger and replicate.
- Click on Next.
Step 3: Add the function
- Provide a name for your new Lambda function.
- Replace the existing code with the one below and adjust the headers to your needs.
- Select Choose an existing role, if you already have a IAM role.
- Select Create new role from template(s), if you have no IAM role yet and want to quickly get started.
- Provide a name for the new role.
- Under Policy template add the Basic Edge Lambda permissions policy.
Step 4: Review
- Review your new Lambda function
- Enable it :)
What about the Content-Security-Policy?
We intentionally did not set the Content-Security-Policy (CSP) header via the Lambda function. Instead we are using a Meta-tag in our HTML file. The reason for this decision was that the HTTP Security Headers configured above are not changing frequently. However the CSP has to be adjusted regularly to accommodate the addition of new external services. Furthermore we want to keep the CSP as strict as possible, which requires a different policy for our development, test and production system. It proved to be easier to dynamically adjust the Meta-tag within the frontend development workflow instead of maintaining multiple or a dynamic Lambda function.
Below you find an example for a very strict CSP. You’ll most probably need to add several sources for styles, scripts and remote connections to adjust the policy for your web application.
You have now increased the security of your CloudFront distribution by attaching several HTTP Security Headers to the viewer response and thereby implemented a security best practice. You can verify your configuration on pages like Mozilla Observatory.
- AWS Documentation — Cross-Origin Resource Sharing (CORS): http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html
- AWS Documentation — Lambda@Edge: http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html
- Serving custom headers from static sites on CloudFront/S3 with Lambda@Edge (deprecated): https://firstname.lastname@example.org/edge-lambda-cloudfront-custom-headers-3d134a2c18a2
- Tutorial: Using CORS with CloudFront and S3: http://blog.celingest.com/en/2014/10/02/tutorial-using-cors-with-cloudfront-and-s3/