I'm a big fan of the new static web hosting feature of Azure Storage, it makes hosting static websites a breeze and brings the costs down to pennies. I have been quick to recommend using this approach until Barry Dorrans highlighted to me that you cannot set headers using this service, more specifically he wanted to set a number of security headers.
This got me thinking, can I combine Azure functions proxy in front of a storage account to add the headers whilst retaining the benefits of static hosting? As it turns out you can.
I should add this is more of a experiment/proof of concept rather than a recommended implementation.
For my use case I had the following requirements:-
- Host static website content
- Cheap as possible, pay for usage only ("serverless pricing") - therefore I don't want to run webserver myself and have to pay for it.
- Must be able to specify response headers
- Have ability to use custom domains
- Have ability to use/enforce TLS
Although some of these requirements can be achieved with either traditional webserver approach (e.g. App Service) or by using storage account static hosting, neither one of those approaches address all of the requirements.
Hosting static pages on Azure storage
As mentioned in the introduction, Azure storage recently enabled the ability to host static websites; this simplifies running websites as you don't need to be responsible for configuring a webserver and can significantly reduce the costs too. The problem with this feature is that it doesn't allow for the setting of headers (please upvote this feature!), in particular it's good practice to set http security headers.
Before the static hosting feature was enabled it was already possible to host static content over http(s) on Azure storage just without the nicer features of default pages and error pages.
Protecting the origin
Because I want to ensure all the website traffic goes through the proxy (so the headers can be added) I don't want to expose the storage container publicly, therefore I can't use the static hosting feature I mentioned earlier. Instead I need to create a normal blob storage container with private access which I can then access from the functions proxy.
To allow the functions proxy to access the storage account you need to create a Shared Access Signature (SAS) which you will use to allow the functions proxy to access the storage.
Azure functions proxy
Azure functions proxy allows you to define a route pattern in an Azure Function application which can act as a proxy to another resource, the proxy allows you to override request and response parameters including headers.
In this example the proxy allows us to securely access our storage account pages and and add response headers to each response.
This is what my proxy looks like once I've setup the routing and headers
Note: I've moved the storage account address and SAS key into Application settings which makes this much easier to manage.
I not only add new headers (e.g. HSTS) but also to redact the values for existing headers that I would prefer not to display (sadly you can't remove headers using the functions proxy).
Testing the security headers
To test the security headers I used the fantastic Security Headers site created by Scott Helme. I would highly recommend using the resources Scott has created to properly configure your security headers, the values I'm using are just to prove the concept and aren't necessarily the best values to use.
To start, this was the score for before adding the response headers. As you can see its not a great score and we're missing a number of important security headers.
After adding the response headers using the Azure Functions Proxy we can achieve an A+ and we've implemented all the headers that this particular scanner was looking for.
One of the reasons for going down this route was to make the hosting cost effective and all of the services used in this approach are charged for consumption only; this means you only pay for what you use and the more popular your site is the more you would have to pay (which is fair enough!).
Providing a cost estimate without knowing the precise usage is a little tricky as the price depends on a number of factors! Suffice it to say its cheap, I would estimate in the region of pennies to a few pounds for a simple low traffic site which is serving a few simple pages. The important bit is that the costs scale with the consumption.
Let's try and break down the cost structure to give you a rough indication on how much this would cost
You pay for data leaving Azure datacenters - the first 5GB/month is free, then £0.065 per GB. You can significantly reduce this by using a service like Cloudfare to provide edge caching (see below).
You pay for the space used in the storage account and for the operations on the storage account i.e. reads and writes. Storage is £0.0147 per GB/month and reads are £0.0033 per 10,000
Azure functions proxy
We're using the consumption plan in Azure Functions which means we don't pay anything if there is no usage. In addition we get the first 400,000 GB/s of execution and 1,000,000 executions are free, beyond this the pricing is £0.15 per million executions.
As you can see from the pricing we are paying fractions of a penny invocations with a generous free allowance for some services, even several million hits wouldn't break the bank.
As the content we are serving is static it makes a lot of sense to cache this information, this not only improves the responsiveness for our users, it reduces our costs further because traffic never reaches our solution.
You can use Azure CDN to provide caching, but I'm a fan of Cloudflare which has a free tier which allows you to cache your site agressively using Cloudflare. Because Cloudflare is serving the requests from its cache this traffic is not hitting your site at all and therefore saving you money!
It would better if I could remove certain headers instead of overwrite them but at the time of writing this isn't possible. If I was using a hosted plan for Azure functions I could use extensions to remove certain headers but I don't believe I can add those other headers very easily (and at that point I would be paying for an app service at so I would just use a normal web hosting approach!).
Another option is to use Cloudflare workers (https://scotthelme.co.uk/security-headers-cloudflare-worker/) - this approach looks cool, especially if you're already using Cloudflare but there is a minimum £5 cost.
I was able to overwrite all the headers in the response except for the X-Powered-By header, for some reason this header wasn't being overwritten by my proxy and in fact it was being duplicated, I've logged an issue for this on GitHub.
There isn't a great story for handling error pages compared to the static hosting feature but I can live with this.
I think this is an interesting approach to solving the problem of not being able to add headers, obviously it would be much better if this was possible directly in the static website hosting feature of Azure storage (please upvote this feature!).
Although I don't go into it in detail in this post you can bind custom domains to the Azure functions app and use SSL (again I prefer to use Cloudflare to provide the free SSL).
Finally, I'll reiterate, the headers I show above aren't necessarily best practice, they are just added to show the principle.
You can find the Azure Functions proxy.json file here: