How to fix the HTTP response headers on Azure Web Apps to get an A+ on securityheaders.io

Summary

In this article we're going to see how to fix the HTTP response headers of a web application running in Azure App Service in order to improve security and score A+ on securityheaders.io. This will involve adding some new headers which instruct the browser to behave in a certain way and also removing some unnecessary headers. There's a brief explanation of how and why we're modifying each header and some links for further reading. Using this site as an example, I'll explain how I modified the headers and I'll show you (most of) the web.config file I'm using to do it. By this point the irony of my telling you everything that I've just worked so hard to conceal will almost certainly not have escaped you.

Finally, when we've finished, we'll use the free securityheaders.io service again to check it worked. Spoiler alert: it did.

Note that the steps here can easily be replicated in IIS on a server too, but it's more fun in Azure and, as I mentioned in my Create your own free reverse proxy with Azure Web Apps article, you can do it for free. In fact it's now possible to trial Azure for up to an hour without even creating an account, simply by going to https://tryappservice.azure.com/.

Background

Whilst it's generally agreed that security through obscurity is not an acceptable information security strategy, it's also true that it's not a good idea to give away too much private information to all and sundry. For example, you don't go around telling people the PINs for your ATM cards or the location of the spare key for your house (e.g. under the plant pot by the front door1). It's also important to realise that the first of these is not an example of security through obscurity; I can't withdraw money from your account with just your PIN (I need the card or a clone of it). However, the second one is; in this scenario the only thing preventing somebody from emptying your house is your forlorn hope that they won't take more than the most cursory of glances in the nearest obvious place. Seriously though, if you are one of those people that does that sort of thing with your front door key, you're an idiot. But I digress.

Why does it matter what my HTTP response headers say?

It might not matter but, for the most part, these headers are not necessary. They're simply extra data that gets sent with each response (not just once per page). Also, thinking about zero-day vulnerabilities, it's easy to see the folly of telling everybody which versions of which software you're running.

What is securityheaders.io?

https://securityheaders.io/ is a free service from Scott Helme which scans your website, examining the HTTP response headers and gives you a grade from A+ to F. This is somewhat reminiscent of the SSL Labs tool from Qualys (which I explained in more detail in my article Why it's really cool that Azure Web Apps now gets an A in SSL Labs), although it's a much more basic service and consequently much faster, too.

Let's take a look at how my blog fared last week.

Before securityheaders.io report for TomSSL Before

Oh, that's not very good (to put it mildly). Note the Server header at the bottom of the image which reveals that we're running on Microsoft-IIS/8.0. There was more bad stuff, but you don't need to see that now. Scan a few sites and see for yourself.

Additional Headers

Scrolling down reveals some useful information about the missing headers which we ought to add.

TomSSL Missing Headers

Clicking on each of these links takes you to a related blog post from Scott explaining more about each one. Since you can't click on that screenshot, I'll repeat the links here:

I'll probably write a bit more about some of these in the future, but for now, let's just get on with fixing our issues.

HTTP Strict Transport Security

This website forces all traffic to go via HTTPS by issuing a 301 Permanent Redirect to any traffic that comes in via HTTP. HTTP Strict Transport Security (HSTS) is a way of preventing that extra trip to the server by getting the browser to issue a 307 Internal Redirect and forcing it to go via HTTPS in the first place.

307 in Chrome

There are a number of posts on the internet talking about how to enable HSTS in IIS. The correct way to do it is to create an outbound rewrite rule and the easiest way to do that is to modify the web.config file like this (irrelevant parts omitted):

<configuration>  
  <system.webServer>
    <rewrite>
      <outboundRules>
        <rule name="Add Strict-Transport-Security only when using 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>  

Content Security Policy

This is another outbound rule and is added in a similar fashion. I am going to write more about this in a future article.

<configuration>  
  <system.webServer>
    <rewrite>
      <outboundRules>
        <rule name="CSP">
          <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" />
          <action type="Rewrite" value="default-src 'self'; <!-- plus all the other stuff -->; report-uri https://domain.com/report;" />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>  

HTTP Public Key Pinning

HPKP protects against Man-in-the-middle (MITM) attacks using rogue X509 certificates by specifying the hashes of the public keys of the certificates that are trusted. Clearly a MITM attacker could modify this header but, since the values are cached by the browser on a trust on first use (TOFU) basis, as long as the correct values are received the first time the user visits the site, then this malicious modification will have no effect.

<configuration>  
  <system.webServer>
    <rewrite>
      <outboundRules>
         <rule name="Add PKP only when using HTTPS">
          <match serverVariable="RESPONSE_Public-Key-Pins" pattern=".*" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <action type="Rewrite" value='pin-sha256="wUFxwf12hU0xHiq8Cgz3fv/Y4H3DwaVl22RC1+n0/h4="; pin-sha256="ntPCN1f+CZzlQhaIE331czBRcAjdmi504yTaH4mK2Gw="; max-age=2592000; report-uri="https://domain.com/report"' />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>  

I'm going to write more about CSP and HPKP in a future article. For example, did you notice the different formats for the report-uri? How annoying.

Adding and removing the custom headers

X-Frame-Options; X-XSS-Protection; X-Content-Type-Options

These can all be added (and removed) by modifying the customHeaders section of the web.config as follows. Note how the unwanted headers are removed too.

<configuration>  
  <system.web>
    <httpRuntime enableVersionHeader="false" /> <!-- Removes ASP.NET version header. Not needed for Ghost running in iisnode -->
  </system.web>
  <system.webServer>
    <security>
      <requestFiltering removeServerHeader="true" /> <!-- Removes Server header in IIS10 or later and also in Azure Web Apps -->
    </security>
    <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" />
      </customHeaders>
      <redirectHeaders>
        <clear />
      </redirectHeaders>
    </httpProtocol>

The (almost) entire web.config file is as follows:

<?xml version="1.0" encoding="utf-8"?>  
<configuration>  
  <system.web>
    <httpRuntime enableVersionHeader="false" />
  </system.web>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <clear />
        <add name="X-Frame-Options" value="SAMEORIGIN" />
        <add name="X-Xss-Protection" value="1; mode=block" />
        <add name="X-Content-Type-Options" value="nosniff" />
      </customHeaders>
      <redirectHeaders>
        <clear />
      </redirectHeaders>
    </httpProtocol>
    <httpErrors existingResponse="PassThrough" />
    <rewrite>
      <rules>
        <rule name="ForceSSL" 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" />
        </rule>
        <rule name="Add PKP only when using HTTPS">
          <match serverVariable="RESPONSE_Public-Key-Pins" pattern=".*" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <action type="Rewrite" value='pin-sha256="K5cLRLJx5XMmt3FZ4juyw6w77/ZS+AP52Q/mK+UO3P0="; pin-sha256="CzdPous1hY3sIkO55pUH7vklXyIHVZAl/UnprSQvpEI="; pin-sha256="ntPCN1f+CZzlQhaIE331czBRcAjdmi504yTaH4mK2Gw="; max-age=2592000; report-uri="https://domain.com/report"' />
        </rule>
        <rule name="Change Server Header"> <!-- if you're not removing it completely -->
          <match serverVariable="RESPONSE_Server" pattern=".+" />
            <action type="Rewrite" value="Tom's 1337 Blog Server" />
        </rule>
        <rule name="CSP">
          <match serverVariable="RESPONSE_Content-Security-Policy" pattern=".*" />
          <action type="Rewrite" value="default-src 'self'; <!-- plus all the other stuff -->; report-uri https://domain.com/report;" />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>  

After securityheaders.io report for TomSSL After

And you can check it for yourself at https://securityheaders.io/?q=tomssl.com&followRedirects=on

Note: In order to achieve this you need to fix the CSP and HPKP bits separately. More on that in a future article.

Conclusion

If you're responsible for a web application then you need to take steps to ensure the safety of yourself and your users. By removing unnecessary HTTP response headers you make it harder for a would-be attacker to find out information about your system. It's also possible to add extra headers to prevent some quite sophisticated attacks such as Cross-Site Scripting (XSS) and Clickjacking. In this article I showed you how I hardened the HTTP response headers for this site (which is hosted in Azure Web Apps) so that it now scores A+ at securityheaders.io, as evidenced by this link: https://securityheaders.io/?q=tomssl.com&followRedirects=on.

Why not subscribe for updates (this is a new feature)? I promise I won't bombard you with spam. In fact it's quite possible I won't ever email you at all. And be sure to follow me on Twitter for more frequent updates:


  1. Just to be clear, there are no spare keys to my house in the garden, not even in one of those stupid hollow plastic "rock" things.