Add security headers to your static websites using Cloudflare Workers

Cloudflare Workers is a service that allows you to run code on every request to your site, we can use this approach to modify the response from your site and add security headers with minimal effort and overhead. Cloudflare workers also have a free plan so this can be done at zero cost (assuming you stay below the limits).

Introduction

I’ve written about static website hosting a few times in this blog, it’s an approach I really like and use for my own sites. One of the limitations of many static hosting services (such as Azure storage static hosting, GitHub Pages and even the new Azure Static WebApps) is that they do not offer any options for controlling security headers leaving us to figure this out. I’m not going to cover the details around specific security headers here - I’d recommend checking out Security Headers to learn more.

I’ve previously written about using Azure Functions proxy to add these headers, that approach was a proof of concept at best (a dirty hack in reality), worryingly I’ve had several emails from folks trying to implement what I described - doh!

I wanted to revisit the topic and show another approach that is very easy to implement, especially if you already use Cloudflare as your CDN (as I do).

There are other approaches to adding the headers depending on where you are hosting the sites, I’ll cover some of these in follow up posts.

Getting started

Cloudflare Workers is a powerful serverless platform, we’ve not going to explore it much beyond our use case here. You can in fact host your entire static site on this platform if you wanted to (you need to upgrade to the paid plan however).

This is my blog (hosted in Azure storage static hosting) before I implemented the Cloudflare Worker to add headers, as reported by the Security Headers scanner.

security headers before Cloudflare workers

As you can see I’m missing many of the important headers, so let’s see how we can address this with Cloudflare Workers. As I mentioned earlier I already use Cloudflare (free plan) so once I’m in the Cloudflare dashboard I can see the Workers menu option

Workers menu option in Cloudflare dashboard

From here I can manage the workers and also assign the workers to routes, we’ll come back to the routes option a little bit later.

To create our worker we need to select the Manage Workers option.

Manage workers page

From the manage workers page we can see all of our workers, configure the subdomain of the workers and see details of your workers invocations and performance, let’s select Create a Worker.

This takes us into the main screen for creating a worker. Essentially we have an editor window on the left and some panes to execute the worker on the right.

Create workers page

For our particular use case we want to add security headers to the response coming back from our origin.

The code we need to use is something similar to this:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {

  let originalResponse = await fetch(request)

  // pass in the original response so we can modify some of it.
  let response = new Response(originalResponse.body,originalResponse);

  response.headers.set('X-Frame-Options', 'SAMEORIGIN');
  response.headers.set('Referrer-Policy','same-origin');
  response.headers.set('Content-Security-Policy','<your-csp-here>');
  response.headers.set('Feature-Policy','camera \'none\'; geolocation \'none\'; microphone \'none\'; payment \'none\'; usb \'none\'');

  return response
}

At the top we are adding an event listener to invoke our function every time there is a request. The handleRequest function first makes the request to our origin but captures the response and modifies it to add the security headers we want before returning the modified response.

I would recommend testing that your headers are working correctly by invoking the worker from the middle pane, you won’t see much when it runs, in fact you’ll probably get a 523 Origin Unreachable error which is fine, you just want to see the headers in the response pane (it’s very easy to incorrectly escape one of the quotes resulting in a broken worker).

You could do clever things here like dynamically constructing your policies based on the incoming host headers but I’d recommend keeping it simple if you can (also worth nothing that there is a limit on the CPU time your worker consumes).

Once we’re happy with our code we can press the Save and Deploy button which will deploy it to the Cloudflare datacentres and make our worker available for us to use, however we’re not quite done yet.

Using the worker

So far we’ve just created and deployed a worker which modifies the headers, the next step is to ensure this worker is executed whenever someone tries to access our site by setting up routes.

Back in our Cloudflare dashboard workers page we now select Add route

Workers menu option in Cloudflare dashboard

From the popup we can configure the path to match (you can use wildcards to match subdomains and paths) and from the dropdown you select the worker you want to invoke

routes page

Take note of the rate limit options these are important, see the rate limit handling section below for more details.

With the route setup our site is now having the headers being applied. We can go back and see how we’re doing now on securityheaders.io

Scan results after applying the headers

We can easily make changes to these headers and see the effects very quickly by modifying the worker and redeploying. If we do end up messing something up we can quickly remove the headers too or simply remove the worker from the request path altogether.

Monitoring the workers

With our worker in place we can keep an eye on the quotas our worker is consuming from the Manage workers page (remember your quota is across all workers and you can have up to 30 of them).

monitoring the quota usage of all the workers

From this screen you can also update your worker’s subdomain and name your workers (by selecting the relevant worker) as well as editing the worker code.

Naming your workers helps you pick the correct worker when you’re setting the up the routes if you have multiple workers.

You can also see performance of individual workers by selecting them

monitoring the performance of your worker

Content Security Policy Header

In my opinion, one of the trickiest headers to get right is the Content Security Policy (CSP). Building a CSP is a little out of scope of this post but I wanted to mention that I used Report URI as it contains a number of tools to help build your CSP policy (and has many other useful features too).

It is also worth noting you have the option of using the Content-Security-Policy-Report header to help you test and refine the policy before you implement it (getting your CSP policy wrong could break your site spectacularly).

Don’t just apply a very liberal CSP policy that effectively allows everything, you are losing much of the benefit of the CSP which is designed to try and protect you against any number of attacks.

Pricing of Cloudflare Workers

Since I last looked at Cloudflare Workers they have introduced a free plan where you can run up to 30 workers across 100,000 requests/day.

My blog doesn’t have a huge amount of traffic so it’s likely to not cost me anything to run. If you want to move to the paid plan it will cost $0.50/million requests per month (with a minimum charge of $5/month).

Rate limit handling

It’s possible you may hit your rate limit for Workers (e.g. 100K requests per day on the free plan), you need to ensure you configure what happens in this scenario. You have 2 choices:

  1. The worker is ignored and your site continues to be available without the security headers.
  2. Your site becomes unavailable.

(reminder: the quota is across all of your workers)

Hopefully you can see there is a possible attack vector here.

If you choose option 2 someone could easily bring your site down by exhausting your quota (a denial of service attack).

If you choose option 1 an attacker could remove the protections of the security headers by exhausting your worker quota and then perform the attack. This approach would take a little more coordinating as there is a limited time window in which to operate (albeit they’d have all day!), however this isn’t my area of expertise and I’ll leave that to others.

Remember for production sites you should consider the paid plan instead which mitigates some of these issues.

Call to Action

Static hosting services such as the new Azure Static Webapps should support this feature out of the box, at the time of writing they do not (although it’s still in preview at the moment).

Please take a moment to upvote this user voice and ask for support built in.

We should be able to define a headers file in our static resources and the hosting infrastructure should be able to simply pick that up and apply them.

It’s worth mentioning that Netlify allows you to do something like this already and it would be great if other providers also supported this.

Wrap up

This post described an approach for adding security headers using Cloudflare Workers, if you’re already using Cloudflare it may be one of the easiest ways you can add the headers. Although we don’t cover it here you can deploy these headers through your deployment pipelines using the Cloudflare Workers CLI - Wrangler

Huge thanks to Scott Helme for all the great resources on security headers and for securityheaders.io and Report URI.

If you have any questions or follow ups feel free to get in touch via the links in the banner and keep an eye out for other posts on this topic.