Being able to create a static website hosted on AWS S3 and fronted by Amazon CloudFront has become the serverless standard these days. Both of these AWS services facilitate a lot of the heavy lifting for you by giving you performant web hosting, with features like caching at edge locations closer to your end users, and security features like mitigating DDoS attacks.
One thing that required a little more effort then I’d prefer was the ability to add security headers to responses. That used to have to be done through Lambda@Edge which, while still serverless, ideally it should be reserved for more computationally involved origin resolution or response manipulation. Today you can now do that with Amazon CloudFront Functions.
Amazon CloudFront Functions provide short lived simple JavaScript functions that can manipulate your response/request. Some use cases could be url rewrites/redirects, access authorization and what I will show you here, header manipulation. See Danilo Poccia’s blog post for more details around Amazon CloudFront Functions.
Keep reading to see how we add the following security headers to our requests using AWS Cloudformation and Amazon CloudFront Function
- Strict-Transport-Security
- Content-Security-Policy
- X-Content-Type-Options
- X-Frame-Options
- X-XSS-Protection
Create your Serverless function
In CloudFormation there are a few things you need to define. 1) The name of your function, in my case add-security-headers
. 2) that you want this to be published automatically. 3) Your function configuration which includes what run time it will use, which at the time of writing this only includes cloudfront-js-1.0
. And lastly 4) your actual code. See the complete resource definition below
addSecurityHeadersFunction: Type: AWS::CloudFront::Function Properties: Name: add-security-headers AutoPublish: true FunctionConfig: Comment: Adds security headers to the response Runtime: cloudfront-js-1.0 FunctionCode: | function handler(event) { var response = event.response; var headers = response.headers; // Set HTTP security headers // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; // Return the response to viewers return response; }
Link your CloudFront Distribution
Now you can update your existing CloudFront Distribution by associating the function to your CacheBehavior
as below and deploy your cloudformation. In my example below I’m importing the ARN from a separate template that includes all my CloudFront Functions.
FunctionAssociations: - EventType: viewer-response FunctionARN: !ImportValue 'Fn::Sub': ${CloudFrontStackName}-AddSecurityHeadersFunction
Test your implementation
Once completed head over to your terminal window and use curl to test out our headers:
ivonne@my-machine ~ % curl -i https://static.ivonneroberts.com HTTP/1.1 200 OK Content-Type: text/html Content-Length: 87 Connection: keep-alive Date: Fri, 28 May 2021 23:59:19 GMT Last-Modified: Fri, 28 May 2021 23:06:46 GMT Etag: "7875df45e56965139099615f6c5c907b" Accept-Ranges: bytes Server: AmazonS3 Via: 1.1 3dc5af024af63cc0e8b9cf31fd852ecf.cloudfront.net (CloudFront) Content-Security-Policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' Strict-Transport-Security: max-age=63072000; includeSubdomains; preload X-Xss-Protection: 1; mode=block X-Frame-Options: DENY X-Content-Type-Options: nosniff
As you can see our 5 security headers are now displayed:
Content-Security-Policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
Strict-Transport-Security: max-age=63072000; includeSubdomains; preload
X-Xss-Protection: 1; mode=block
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Conclusion
Adding a Amazon CloudFront function is pretty simple and you can see the updates almost immediately. If you would like to see the full CloudFormation templates head on over my github (link below). Feel free to modify it to fit your use case. As always if you have any questions reach out!
Github: https://github.com/ivlo11/serverless-patterns/tree/main/static-site-with-cloudfront-function
Documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html
CloudFormation Documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-function.html
CloudFront Function Sample Code: https://github.com/aws-samples/amazon-cloudfront-functions
Great write-up!!
If a response can be cached, would it be better to add the security headers part of Lambda@Edge (origin-respone)? So that the headers are added only once and subsequent cache hits are served with the same headers.
I think that is a fair approach. To make that decision, I think a few things need to be taken into account:
I’m sure I’m missing other comparison points, but basically, I think at the end of the day, this isn’t a one-size fits all approach. Each team would have to evaluate the two against each other.