Add Security Headers to your Serverless Static Site

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

Happy Coding!

2 Replies to “Add Security Headers to your Serverless Static Site”

  1. 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.

    1. I think that is a fair approach. To make that decision, I think a few things need to be taken into account:

      1. It would be important to look at your site’s traffic/caching policy to be able to compare costs between Lambda@Edge (6x more expensive) versus CloudFront Functions.
      2. CloudFront functions are basically paired down functions with no network access, that runs in a process versus lambda in a VM, which makes me believe that startup/initialization is much faster than a Lambda – think less (or maybe imperceivable?) performance impact to end-user.
      3. Lambda@Edge is not in every edge location, so depending on where your traffic is being served, one might be better than the other.

      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.

Leave a Reply

Your email address will not be published. Required fields are marked *