Testing Web Server Headers

securityheaders.io is a cool site to test the hardening of your web server. I suspect that many sites would score fairly low and although you could debate the relative value of each measure, why not aim for the top and make an attacker's life that much harder.

Well, our test site has just scored A and I hope to increase that to A+ once I have sorted out the Content Security Policy using my cool ASP.Net CSP builder!

Most of the headers are pretty easy. If you only use https for instance, you might as well set Strict-Transport-Security, which is cached for a period of time and tells the browser only to allow access to this site via https. This is cached in the browser and can be un-cached but helps an unwitting user from suffering some kind of downgrade by a MITM talking to our site with https and the victim's browser with http. Anyway, it's easy to add, I do it in Application_BeginRequest in my application.

Most of the others are pretty easy to and can be applied to most of our sites to prevent things like X-Framing and some amount of XSS protection although those that need to be applied to the entire server, rather than just the app, need to be added either manually to IIS via the interface or in Azure, using a startup script and either command-line or PowerShell.

I have these lines in my startup.cmd:

%windir%\system32\inetsrv\APPCMD.EXE set config -section:system.webServer/httpProtocol /+"customHeaders.[name='X-Frame-Options',value='DENY']"
%windir%\system32\inetsrv\APPCMD.EXE set config -section:system.webServer/httpProtocol /-"customHeaders.[name='X-Powered-By']"

Adding the header for denying framing and removing the one that tells the client what version of ASP.Net I am using!

IMPORTANT NOTE: You MUST save your startup.cmd as ASCII, otherwise the unicode header will confuse Windows command and appear as a weird character, which will break the first line of functionality and possibly have knock-on effects depending on its content. Click save-as and on the save button, choose "save with encoding" and I used US-ASCII as the encoding.


Public-Key-Pins is slightly more complex to carry out because it requires taking signatures of our SSL certificates and giving them to the browser. In a similar way to Strict Transport Security, the browser will remember these signatures for the amount of time we specify and will require that the signature of the SSL certificate match one of those specified in the header. The article here explains the process of signature gathering better than me but I want to then explain how you can do this on Azure using the startup task.

Backup Signatures

It is VERY important that you understand the purpose of back-up signatures. Imagine that you have told the browser to remember your pins for, say, 6 months. 2 months later, your site is hacked and an attacker has obtained the private key to your SSL certificate and now has the ability to decrypt communications between your customers and you. You decide to revoke the SSL certificate and you now need to obtain another one but you cannot use the same private key, because it was compromised, you instead generate a new one, get a new SSL certificate and deploy it. The browser does not know this and will insist that the new certificate matches one of its pinned signatures, which it won't UNLESS you create, say, 2 new CSRs for certs, add their signatures to your list, so that if the cert is compromised, you use one of these CSRs to get your new cert and its signature will already be valid. The linked article describes this, just make sure not to bypass it otherwise you will have irate customers asking how to clear the pin cache on their browser to access your site!

IIS and Azure

It's pretty easy in IIS to add the header that you have creating for your certificate signatures, you just open IIS Manager and choose "Reponse Headers" and add a new one with the correct key and value but in the case of Azure, you will want to automate this and apply it to the entire server, not just the app, so it applies to any resources that are not obtained from the application. This takes us back to our startup task.

Hopefully, you already have one of these to run various things whenever the role is started or restarted (because of this, make sure you don't always reboot after anything, otherwise you will cycle forever). I use something called startup.cmd, which is set to always copy to output in Visual Studio. I then have these lines in my ServiceDefinition.csdef:

<Startup priority="-2">
<Task commandLine="startup.cmd" executionContext="elevated" taskType="simple" />

This lives under the WebRole section and tells the system to run the task with elevated permissions and simple just means that it runs as you would expect, rather than a foreground or background task.

You can then put whatever you need into it, including calling other scripts if required. I do the following in mine:

  1. Add X-Frame-Options DENY
  2. Remove X-Powered-By
  3. Add my Public-Key-Pins
  4. Add the web IP security IIS feature so I can...
  5. Enable IP security section to be able to dynamically blacklist IPs
  6. Enable dynamic IP security to provide rate-limiting
  7. Replace the default Location header (containing internal IPs) with a named URL
  8. Setup the best-practice for SSL cipher order using a Powershell script
  9. Install a stripheaders module to remove default Server header from IIS
The code for adding the public-key-pins is standard AppCmd stuff but NOTE that the double-quotes in the pin values need to be tripled " => """ so that they escape properly on the command line. This is probably not required in Powershell but I don't know.

%windir%\system32\inetsrv\APPCMD.EXE set config -section:system.webServer/httpProtocol /+"customHeaders.[name='Public-Key-Pins',value='pin-sha256="""wCJKYZwKJ8LcMVKrPHv+0cBua/ndTZ3aUeogjN6S0xI=""";max-age=2592000']"

I have removed two of the pins to make it simpler to read but you should get the idea! The max-age above is 1 month in seconds, which I will use for now.

The link recommends starting with a short max-age so that any problems can be fixed quickly and easily, you can also append "-Report-Only" to the header so that the browser will log any problems but not block you, this is ideal for testing that you have the correct signatures and format for the header. Another reminder to use the backup CSR process so that you can easily reset your SSL cert if a problem arises.