Hardening HTTPS with nginx

I’ve improved my HTTPS setup with nginx recently. For a start I’ve organised the files better. For a TL;DR I’ve put the pertinent files on GitHub.

First I have conf/nginx.conf, the main configuration file, which defines lots of mundane non-security related things. Then the penultimate directive is: include includes/tls.conf; This defines the various TLS rules globally. In particular this allows the session cache to be shared amongst several virtual servers. Let’s take a look at what else is done here:

# Let’s only use TLS
ssl_protocols TLSv1.1 TLSv1.2;
# This is sourced from Mozilla’s Server-Side Security – Modern setting.
ssl_prefer_server_ciphers  on;

# Optimize SSL by caching session parameters for 10 minutes. This cuts down on the number of expensive SSL handshakes.
# The handshake is the most CPU-intensive operation, and by default it is re-negotiated on every new/parallel connection.
# By enabling a cache (of type \"shared between all Nginx workers\"), we tell the client to re-use the already negotiated state.
# Further optimization can be achieved by raising keepalive_timeout, but that shouldn't be done unless you serve primarily HTTPS.
ssl_session_cache    shared:SSL:10m; # a 1mb cache can hold about 4000 sessions, so we can hold 40000 sessions
ssl_session_timeout  24h;

# SSL buffer size was added in 1.5.9
ssl_buffer_size      1400; # 1400 bytes to fit in one MTU

# Use a higher keepalive timeout to reduce the need for repeated handshakes
keepalive_timeout 300; # up from 75 secs default

# SPDY header compression (0 for none, 9 for slow/heavy compression). Preferred is 6. 
# BUT: header compression is flawed and vulnerable in SPDY versions 1 - 3.
# Disable with 0, until using a version of nginx with SPDY 4.
spdy_headers_comp 0;

# Diffie-Hellman parameter for DHE ciphersuites
# `openssl dhparam -out dhparam.pem 4096`
ssl_dhparam includes/dhparam.pem;

As you can see, I don’t support any version of SSL. It’s insecure. I’ve also dropped support for TLSv1. I’m still undecided on that. Remember you are going to need to generate your own dhparam.pem file. This command can take a long time.

In each included virtual server I further include two other files, stapling.conf and security-headers.conf. The first file is very self explanatory and simply enables OCSP Stapling. As far as I can tell for nginx, if you use virtual servers, one of them needs to be designated a default_server and at least this one needs stapling enabled in order for stapling to work for any other virtual server. Feedback on this point is welcome.

The second file, security-headers.conf, is where I improve the security of the sites using several HTTP headers. I’ve been particularly inspired by securityheaders.io. Let’s take a look:

# The CSP header allows you to define a whitelist of approved sources of content for your site.
# By restricting the assets that a browser can load for your site, like js and css, CSP can act as an effective countermeasure to XSS attacks.
add_header Content-Security-Policy \"default-src https: data: 'unsafe-inline' 'unsafe-eval'\" always;

# The X-Frame-Options header, or XFO header, protects your visitors against clickjacking attacks.
add_header X-Frame-Options \"SAMEORIGIN\" always;

# This header is used to configure the built in reflective XSS protection found in Internet Explorer, Chrome and Safari (Webkit).
# Valid settings for the header are 0, which disables the protection, 1 which enables the protection and 1; mode=block which tells
# the browser to block the response if it detects an attack rather than sanitising the script.
add_header X-Xss-Protection \"1; mode=block\" always;

# This prevents Google Chrome and Internet Explorer from trying to mime-sniff the content-type of a response away from the one being
# declared by the server. It reduces exposure to drive-by downloads and the risks of user uploaded content that, with clever naming,
# could be treated as a different content-type, like an executable.
add_header X-Content-Type-Options \"nosniff\" always;

This is unashamedly copied from Scott Helme.

There are two more headers I use, but these are used on a site-by-site basis and are thus done in the virtual servers files themselves. This is because once we use these headers we can’t really go back to having a non-https version of the site. You can see them in the sites-available/https.jonnybarnes.uk file. They are the HSTS and HPKP headers. HSTS is easy, it just tells the browser to only use https:// links for the domain. This is cached by the browser, and can even be pre-loaded. HPKP is a little more involved.

The idea with HTTP Public Key Pinning is to try and stop your site being the subject of the Man-in-the-Middle attack. In such an attack a different certificate than yours is presented to the user. In particular the public key included in the certificate is not the associated public key to my private key. What HPKP does is take a pin, or hash, of the public key and transfer that information in a header. This value is then cached by the user’s browser and any subsequent connections the browser checks the provided public key matches this locally cached pin. For fallback purposes a backup pin must also be provided. This backup pin can be derived from a public key contained in a CSR. In particular this CSR needn’t have been used to get a signed certificate from a CA yet.

Scott Helme has an excellent write-up of this process. Given either of your current site certificate, or CSRs for future certificates it’s simply a few openssl commands to get the relavent base64 encoded pin. Then a single add_header directive in nginx.

The end result of all this is a more secure site, hopefully. One issue to note for now is Mozilla Firefox doesn’t support HPKP yet and you’ll get error entries in the console log regarding an invalid Public-Key-Pins header. This should get fixed in time.