Your HTTPS Setup is Broken II: Patch it with HSTS

This post is the second in the “Your HTTPS Setup is Broken”-series. Previously, I’ve described how easy it is for an attacker to eavesdrop on your “secure” communication. In this post I’ll show you how to enforce encrypted communication, so an attacker cannot downgrade the connection to unencrypted HTTP.

The attack vector which we want to mitigate is that somebody intercepts your traffic before it gets switched to HTTPS. Nearly nobody enters “https://www.example.com” into the titlebar. Instead, they’re entering the domain name “example.com” and your server redirects them to HTTPS.

To prevent the user’s browser to switch to HTTPS, a man-in-the-middle could simply strip out all HTTPS-links, -redirects or -form-actions. As the man-in-the-middle is effectively a proxy, it can rewrite any resource links from HTTPS to unencrypted HTTP in order to intercept any requests to these links. All traffic from the man-in-the-middle the server will then get encrypted within an established SSL-session to the server, and all traffic to the user will go over the wire via unencrypted HTTP.

HTTP-Strict-Transport-Security (HSTS)

HSTS is a simple HTTP header which can be transmitted by a webserver. It tells the browser to strictly use HTTPS for the request’s domain at future times, no matter what the user types into the address bar. This directive is stored within the user’s browser for a given amount of time.

As you may have realized now, this procedure implies that the request is made via a uncompromised connection: If the first (and all following) requests are made via a compromised connection and the attacker strips out the HSTS header, the user will never benefit from this security enhancement. This principle is called “Trust on First Use” (TOFU). It was chosen to simplify the process and to get a wider adoption for this feature. As soon as the user has a non-compromised connection, he will receive the HSTS header and from that time will strictly use HTTPS for the given amount of time. Every time he visits your website again, this amount of time will get renewed.

How does it work?

It’s just as simple as this:

Add this header to all of your HTTPS responses:

Strict-Transport-Security: max-age=31536000

This header tells the browser to stricly use HTTPS for your domain. As long as the max-age (in the case above 365 days) isn’t reached, or the browser cache for this domain doesn’t get cleared, the browser always uses HTTPS, even if the user asked for HTTP, didn’t enter the scheme or used an old bookmark.

You can also enable this feature for all of your subdomains, if you want to:

Strict-Transport-Security: max-age=31536000; includeSubdomains

How to implement in NGINX

That’s a no-brainer, too:

If you have separate server configurations for HTTP and HTTPS, you can simply use this:

server {
    listen 80;
    return 301 https://$server_name$request_uri;
}

server {
       listen 443 ssl;
       
       # the "always" parameter was added in 1.7.5. Prior versions don't know this parameter, so you should remove it-
       # if "always" is missing, it wouldn't be set if the status code is not 200, 204, 301, 302 or 304
       add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
   }

If you have the same server configuration for HTTP and HTTPS, you must not add the HSTS header to HTTP, since this is not allowed by the RFC. So in this case you have to add a map-directive to add the header for HTTPS only.

map $scheme $hsts_header {
    https   "max-age=31536000; includeSubdomains";
}

server {
    listen  80;
    listen  443 ssl;

    add_header Strict-Transport-Security $hsts_header always;
}

If you want to add this header to Apache or Lighttpd, please find a manual here.

Test it!

If you haven’t enabled HSTS yet, you should definitely give it a try! But probably you don’t want to test this stuff on your production system. Even with a short amount of max-age, it could prevent users to access your server. This could happen due to missing HTTPS configuration, faulty certificate, etc… And the biggest drawback is: You wouldn’t even recognize this, because the browser will prevent any request to your server.

To the rescue: I’ve set up a vagrant box for local testing, where you can configure and test as much as you want. I also had a credit voucher for a SSL certificate, so I’ve added a valid certificate and private key for the testing domain, which is valid until 29th January 2017.

Feel free to use it: https://github.com/inoio/hsts-hpkp-playground

Pitfalls

While configuring HSTS for our inoio webserver, we found out that the add_headers-directive does not add headers in every case: When you use this directive in a child configuration block, it will overwrite all previously configured headers from the parent! To our rescue, we found this great article from Gert van Dijk, who discovered this problem previously. We decided to use the ngx_headers_more module which solved the problem.

If you want to test your HSTS settings, you have to keep in mind that your browser will only use the headers if it receives a valid certificate for the given domain. Otherwise, it will ignore the header and you won’t see any effect. Example: If your certificate is outdated, the user will see the warning about “Insecure Connection” with the ability to add an exception for this domain. But the HSTS header will be ignored until the browser receives a valid certificate.

Conclusion

HSTS is a minimum security feature everybody should use. It is easy to implement and is transparent for the user. It prevents the user to get hijacked by a man-in-the-middle over an insecure connection when the server wants to use HTTPS.

But: it does not prevent you from SSL certificate forgery. This can be achieved using HPKP (HTTP Public Key Pinning), which I’ll present in the next blog post of this series.

Comments