HSTS with nginx and Varnish

SSL is good. It’s not perfect, but it makes life harder for mass surveillance and all websites should be using it. Yes, I know this blog doesn’t – I’ll get around to it.

I upgraded one of my sites to use HSTS, which is an extension to enforce usage of SSL where it’s available. This effectively means that after the first request via HTTPS, the browser should remember that domain uses SSL and should make sure any subsequent requests are HTTPS. HTTP requests get redirected to HTTPS immediately. This is great – not only does it mean that you’re less likely to have clients making requests in the clear when they should be using SSL, but it means that SSL stripping attacks will be foiled.

This particular site is a bit complex because it uses Varnish to cache responses – mostly images, between 20kb and 4-5MB. There’s a 64GB in-memory cache to keep that site moving rapidly. Varnish, hovever, doesn’t support SSL – so we have to put nginx in front. Thus, an average request comes into nginx via HTTPS, nginx adds the X-Forwarded-For header with the IP of the client and sets X-Forwarded-Proto to ‘https’. Then it reverse proxies the request to Varnish in the clear. Varnish then returns the response from the cache, or forwards it on to nginx in the clear, where the response is either served from disk or proxied from the application servers.

Because Varnish doesn’t speak HTTPS and doesn’t even support redirects we have to do a few little things in the configs I thought I’d document here for the sanity of whoever comes after me.

First up we need to add the following to our nginx config, in our HTTPS server block.

What this does is pass through the client IP and forwarding protocol to Varnish, and sets the Strict-Transport-Security header as defined in RFC 6797. This is half the battle – once we’ve got the request into Varnish and have a way to tell it’s come through the SSL termination layer we can treat that traffic appropriately in Varnish.

In our Varnish config we simply toggle on the X-Forwarded-Proto header and throw an error with code 750 if it isn’t HTTPS, after setting X-Redir-Url on the request to a clone of the URI with a https schema. Then we rescue that in vcl_error, use the header on the request we just set to populate the Location field of the response and return a HTTP 301 code. Job done! It’s worth noting that we don’t verify the request with header is coming from a trusted upstream source – you can identify this sort of spoofing using ACLs and probably already have a nice example in your config if you’re using HTTP PURGE/BAN requests. My config doesn’t use this as forcing connections to use HTTP isn’t a significant security issue in this instance.

Our client now gets redirected to HTTPS on the first HTTP request, and from that point forward will never make another non-SSL request. As it should be!

It’s worth noting, of course, that you can break things with this. I’d strongly recommend a slow start – gradually increase your HSTS policy duration as you run into problems. I ran into problems with the includeSubdomains option of the HSTS header – I thought all subdomains of that site pointed at things that spoke SSL but forgot about one. I opted in the end for not using the includeSubdomains option as everything but that service runs through the same SSL termination point so gets HSTS regardless.