Making an Azure static website EVEN MORE secure

Remember how I was congratulating myself that I’d made my jmbucknall.com static website, that is hosted on Azure, secure? How I’d bought and uploaded an SSL certificate, and made the site only accessible via HTTPS? Well, HA!

I say that, because Barry Dorrans (self-described as “Microsoft's .NET security person”) was ‘kind’ enough to point out that I hadn’t really finished the job. I hadn’t added the proper “security headers” (WTF are they?) via a web.config (wut? it’s a static site!) and that I should go immediately to securityheaders.io to scan my site. Which I did, and got a resounding F. Um, gee thanks, Barry.

So, yesterday, it was time to investigate and to fix and to get an A+.

I’ll admit that a lot of this was brand new to me, but let’s start from the beginning. When you visit a website, your browser builds a request and sends it to the web server that’s serving up the content (HTML, CSS, script files, images, whatever). That request has a set of request headers that describe what the browser can deal with and what it is expecting (things like the type of files, the language, any encodings, and so  on).

Request headers

Example request headers

The web server, in replying, does the same: a chunk of data (HTML, CSS, etc) with a set of response headers. The security headers Barry pointed me to form part of these response headers. Whether the site is static or not, these headers are provided by the web server, not the site, and it is through the web.config file that we can insert new headers or take out the ones we don’t want to send back.

Example unsecure response headers

Unsecure response headers

So, despite my conceit that a static website has no need for a web.config, in fact it does. Think of it in this case as a set of instructions to the web server, rather than being part of the site’s static content.

(Aside: when I started to gather the code for the security headers, I discovered that, in fact, I already had a web.config for jmbucknall.com. I’d completely forgotten I’d added it, and had overlooked it since then. Its only purpose was to declare the MIME type for the .woff font files I use in the site; in other words an instruction to the web server that .woff files are valid.)

Next, then, was to work out which security headers to add and how to define them in my web.config for the site. As it turns out, securityheaders.io makes that easy: it scores you according to which headers your server is returning or not. I was … not, to be blunt.

Security Report

Example security report

Yep, six security headers are missing. Luckily the scan site gives you a quick description for each (which I’ve quoted extensively here).

Strict-Transport-Security

HTTP Strict Transport Security (HSTS) is an excellent feature to support on your site and strengthens your implementation of TLS by getting the User Agent to enforce the use of HTTPS. Recommended value "strict-transport-security: max-age=31536000; includeSubDomains".

To add this to the config file involves a weird-looking Redirect rule (to force a redirect of HTTP to HTTPS) and an equally weird-looking Rewrite rule (once you’ve used HTTPS, always use it). The best discussion of this I found is from Scott Hanselman’s blog a couple of years back.

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="HTTP to HTTPS redirect" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
              <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
        </rule>
      </rules>    
      <outboundRules>
        <rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
          <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <action type="Rewrite" value="max-age=31536000; includeSubdomains; preload" />
        </rule>
... </outboundRules> </rewrite>
... </system.webServer> </configuration>

To be honest, I think that the Redirect is actually already done by setting that HTTPS Only option in the Azure dashboard. One day, when I have a spare 5 minutes, I’ll experiment.

Content-Security-Policy

Content Security Policy (CSP) is an effective measure to protect your site from XSS attacks. By whitelisting sources of approved content, you can prevent the browser from loading malicious assets.

This one is again done through a Rewrite rule, but it involves a whole bunch of extra work in the website’s content files as I found out. I’ll talk about this a bit later.

<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
<rule name="CSP"> <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" /> <action type="Rewrite" value="default-src 'self'; script-src 'self' https://code.jquery.com https://ssl.google-analytics.com; img-src 'self' https://ssl.google-analytics.com; style-src 'self' https://maxcdn.bootstrapcdn.com; font-src 'self' https://maxcdn.bootstrapcdn.com;" /> </rule> </outboundRules> </rewrite>
</system.webServer> </configuration>

Yes, this rule does have a horrendously long value for the action. More on that in a moment.

The next four headers are simple to set as a block.

X-Frame Options

X-Frame-Options tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking. Recommended value "x-frame-options: SAMEORIGIN".

X-XSS-Protection

X-XSS-Protection sets the configuration for the cross-site scripting filter built into most browsers. Recommended value "X-XSS-Protection: 1; mode=block".

X-Content-Type-Options

X-Content-Type-Options stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. The only valid value for this header is "X-Content-Type-Options: nosniff".

Referrer Policy

Referrer Policy is a new header that allows a site to control how much information the browser includes with navigations away from a document and should be set by all sites.

The code for these four looks like this:

<configuration>
  <system.webServer>
<httpProtocol> <customHeaders> <clear /> <!-- Gets rid of the other unwanted headers --> <add name="X-Frame-Options" value="SAMEORIGIN" /> <add name="X-Xss-Protection" value="1; mode=block" /> <add name="X-Content-Type-Options" value="nosniff" /> <add name="Referrer-Policy" value="no-referrer" /> </customHeaders> <redirectHeaders> <clear /> </redirectHeaders> </httpProtocol> </system.webServer> </configuration>

Back to the CSP

If you look back at the CSP code, you’ll see that it has a value for the action that is in essence a set of clauses separated by semi-colons. Each of those clauses defines where a particular content type can be downloaded from. So, for example, the script-src clause defines from where the browser is allowed to download scripts: there’s 'self' meaning the current domain, then there’s one url for jQuery, and one for Google analytics. The other clauses are all pretty self-explanatory. The header is basically saying: ONLY download this particular type of content from these urls. No others.

Of course, when I wrote the content for jmbucknall.com, I just wrote the markup, etc, as I’d always done. Didn’t have to whitelist anything. It just worked. Now, in this new security landscape, I have to, in order to avoid XSS-type attacks. So, what happened was that, once I’d uploaded my new web.config, suddenly, parts of my site no longer rendered properly. I had to go through and fix all those references to “outside” urls.

The other fun bit I had to fix was the Google Analytics code. Be default (at least when I wrote the HTML), it was a bunch of script elements on the page, plus it did a document.write to add some more. Under the CSP, inline scripts are not allowed. (Strictly speaking, they are, but you have to add an option whose very name should strike terror into your secure heart: 'unsafe-inline'.) So, I took those inline scripts out of the HTML and put them in a JS file. But, hey, I don’t use inline scripts in my own markup, never have done (they’re cheesy and cheating), so that was easy.

Secure response headers

Secure response headers

And there we have it, Part Deux on making your Azure-hosted static website secure. Now I’ve done it once, time to experiment with another of my domains, this time with a Let’s Encrypt certificate. Because it’s free or something.

Secure site banner

Loading similar posts...   Loading links to posts on similar topics...

5 Responses

 avatar
#1 Cam Penner said...
05-Feb-19 12:09 PM

I had to teach myself the security headers for a non-static site. It wouldn't have occurred to me to put them on a static one.

I've just migrated to Azure static website using the built in static site hosting with blob storage. Is this azure.microsoft.com/en-us/blog/azure-storage-static-web-hosting-public-preview Is this what you were using here too? I can't seem to get mine to recognize the web.config file. Where did you put it?

julian m bucknall avatar
#2 julian m bucknall said...
05-Feb-19 12:12 PM

Cam: Good question about static site hosting via Azure Storage. As you may have surmised, I did not use Azure Storage to host my jmbucknall.com static site. The biggest reason is that this Azure 'feature' only allows you to host the files that are requested via a browser (so, HTML, CSS, JS, JPG, PNG, etc, etc). web.config is a server-side file, and I'm afraid you don't have access to the web server (in any form) when you use this static hosting on Azure Storage. In other words, you could say my static site is configured to be a (very dumb!) ASP.NET site with no server-side code whatsoever. Yes, it costs more than the Azure Storage option, but you do get that little bit more control over the server.

Cheers, Julian

julian m bucknall avatar
#3 julian m bucknall said...
05-Feb-19 1:46 PM

All: Cam did more googling than I did just now and came up with this blog post: Add Security Headers to an Azure Storage Static Website using Azure Function Proxies. In essence, this is the Azure equivalent to what you have to do with AWS (as I explain in the Making an AWS static website EVEN MORE secure blog post). Now if only I had a spare domain to try this all out on...

Cheers, Julian

 avatar
#4 Cam Penner said...
06-Feb-19 10:58 AM

Using the Azure Function worked perfectly. You set the blob storage using the "static website" method. Then you create a Function that points to the static site and use the proxy filter to insert your new headers. Once that is working, you change your DNS to point to the Function instead of the website. Then you put your Custom Host name on the Function.

If only you could hook up Lets Encrypt free SSL...

 avatar
#5 JP said...
02-Mar-20 11:38 AM

This is a great write-up and was super helpful in implementing these security headers on my companies web application. I have a questions about CSP though. Why did you do CSP via an outbound rule instead of a custom header? While researching, I came across the Content Security Policy website and the example for a web.config there adds the CSP rule under customHeaders rather than as an outboundRule.

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response