(2014-05-02) SSL Security and Nginx

edit (2014-06-01): I've now changed the cipher suite and protocol support to this:

http {
	...
	server {
		...
		ssl_ciphers	ECDHE-RSA-AES256-SHA:!ADH:!AECDH:!MD5;
		ssl_protocols	TLSv1 TLSv1.2;
		...
	}
}

This boosts Cipher strength up to 100 and only drops support for a couple more devices (namely IE8 on XP and the stock browser on Android 2.3.7). It also forces all other browsers to use the exact same suite. I've also disabled TLS 1.1 support as nothing uses it by default.

/edit

I've been working on trying to get a perfect score over at ssllabs (https://www.ssllabs.com), which, depsite the number of test results that don't even get a A- ranking, is a lot easier than it seems.

I had to do a bit of digging around to work out what some of the lesser known tests were actually about (and there are still a couple I'm not sure about).

However, I have now come up with both a 100/100/100/100 config, and a 100/95/100/90 config. The first being really secure, but also really incompatible, and the second being almost as secure but much more compatible.

As I use Nginx, I only know the exact configuration settings for it, though most commands seem to be fairly consistent with Apache, but I haven't looked them all up.

It should also be noted that I had to get my SSL certificate reissued using a 4096bit private key, so that's probably as good a place as any to start.

openssl req -new -nodes -newkey rsa:4096 -keyout private.key -out signing.csr

This will generate the key and CSR needed. All you need to do is send the CSR off to the certificate authority and wait for them to send you the full signed certificate back. I don't know which authorities *don't* support 4096bit keys, but GeoTrust's RapidSSL works fine for me. They do only have a 2048bit Root certificate, but I wouldn't say this was much of an issue. They also allow you to use SHA256 with RSA encryption rather than standard SHA1, which is a nice little extra.

Once you've done that, and you have it installed, if you're *not* using EC/DHE ciphers, then you'll likely get a 100% rating on your "Certificate" immediately. However, you may find that you do actually have EC/DHE ciphers already enabled, and you might not get the full 100% straight away. The next step is to get those ciphers using an extremly strong DH certificate. This is a much more advanced encryption algorithm than even something like SHA256 and put simply a secure way of exchanging keys between the client and server. If you want to read up on what exactly this type of encryption is, you can find the Wikipedia page here (https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange).

By default, Nginx will use a 1024bit DH certificate, which is totally fine. But that doesn't get you the 100%, and (while it's really not very vulnerable) the higher the bit level, the better. So, let's generate a 4k certificate by using openssl:

cd /etc/ssl/certs && openssl dhparam -out dhparam.pem 4096

Obviously, change the directory to wherever you are keeping your certificates and Nginx has access to (you are running in a chroot, right?). Now, this process can take a very long time, depending on your CPU power.

Once done, you need to tell Nginx to use that new certificate, and we do this via the "ssl_dhparam" variable (in the server block):

http {
	...
	server {
		...
		ssl_dhparam	/etc/ssl/certs/dhparam.pem;
		...
	}
}

All fine and good -- now, if a client connects using EC/DHE, they should use a 4k certificate to make the key exchange.

The next step is to fix some common faux pas. We want to make sure the SSL cache is not kept for an age, but at the same time, is not immediately discarded. So we can do with a couple more ssl varaibles in the server block:

http {
	...
	server {
		...
		ssl_session_cache	shared:SSL:10m;
		ssl_session_timeout	10m;
		...
	}
}

Now we want to enable HSTS (http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) which helps negate "man in the middle" attacks. This is done, simply by adding a header like this:

http {
	...
	server {
		...
		add_header	Strict-Transport-Security max-age=31536000;
		...
	}
}

The max-age value is seconds, and the above should be for a full 365 days. This isn't too important, but you can half that if you want.

While we are on the topic of headers, you may want to consider disabling "X-Frame-Options". Which would prevent your site from displaying in a frame / iframe on another site. Simply add this:

http {
	...
	server {
		...
		add_header	X-Frame-Options DENY;
		...
	}
}

Now -- the most important part of your SSL setup, and the difference between 100/100/100/100 score and anything else. The SSL protocols and ciper suites.

I will use the (much) less compatible option here, and then further down include the half and half solution that should cover the vast majority of browsers and situations.

First up, we need to specify the SSL protocols to use. For the 100% "Protocol Support" score, you need to only support TLSv1.2. So, add the following to your server block:

http {
	...
	server {
		...
		ssl_protocols	TLSv1.2;
		...
	}
}

Supporting TLSv1.1 as well will give you a score of 97, and supporting TLSv1.0 and TLSv1.1 as well as TLSv1.2 will give you the 95% score.

Now, the variable that makes or breaks it, the cipher suites. The biggest issue here, is a large majority of browsers don't support the high end ECDHE cipers, but if you can control which browsers are being used on the client end (for example, for an internal service), this shouldn't matter to much. Firefox supports pretty much every ciper suite avaliable, but we want to use the most secure possible. So we do need to use ECDHE. We do this with the "ssl_ciphers" variable:

http {
	...
	server {
		...
		ssl_ciphers	ECDH+AES256:!ADH:!AECDH:!MD5;
		...
	}
}

The above will only include three cipers, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384" and "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA". As far as I can tell, these are the most secure cipers you can possibly use at the moment. They all support forward secrecy and use 256/3072bit asymetric encryption.

However, you will only be able to support OpenSSL 1.0.1e and newer, as well as IE11 and newer, Android 4.4.2 and newer, Chrome 33 and newer, Firefox 27 and newer Safari 6/7 (on iOS) and newer and Safari 7 (on OSX) and newer.

The final thing you want to do is enable "prefer server ciphers"; which tries to force the servers preferred cipers on the client. As usual, this needs to be in the server block of your Nginx config:

http {
	...
	server {
		...
		ssl_prefer_server_ciphers	on;
		...
	}
}

So, once it's all done, you should end up with a config like this:

http {
	...
	server {
		listen				443 ssl;
		ssl_certificate			/etc/ssl/certs/cert.pem;
		ssl_certificate_key		/etc/ssl/private/private.key;
		ssl_session_cache		shared:SSL:10m;
		ssl_session_timeout		10m;
		ssl_prefer_server_ciphers	on;
		ssl_ciphers			ECDH+AES256:!ADH:!AECDH:!MD5;
		ssl_protocols			TLSv1.2;
		ssl_dhparam			/etc/ssl/certs/dhparam.pem;
		add_header			Strict-Transport-Security max-age=31536000;
		add_header			X-Frame-Options DENY;
		...
	}
}

If you have any sense, you'll also redirect http traffic directly to https as well.

Once you've added all that in a restarted Nginx, you should now get a 100/100/100/100 score. Like my site using these settings: (click to see the full report)

edgley.org - ssllabs.com results: 100/100/100/100

Of course, I can't quite bring myself to restrict so many sytems from accessing my site, so I had to make a few changes to the last rules.

http {
	...
	server {
		listen				443 ssl;
		ssl_certificate			/etc/ssl/certs/cert.pem;
		ssl_certificate_key		/etc/ssl/private/private.key;
		ssl_session_cache		shared:SSL:10m;
		ssl_session_timeout		10m;
		ssl_prefer_server_ciphers	on;
		ssl_ciphers			ECDH+AESGCM:ECDH+AES256:DHE-RSA-AES128-SHA:!ADH:!AECDH:!MD5;
		ssl_protocols			TLSv1 TLSv1.1 TLSv1.2;
		ssl_dhparam			/etc/ssl/certs/dhparam.pem;
		add_header			Strict-Transport-Security max-age=31536000;
		add_header			X-Frame-Options DENY;
		...
	}
}

This sacrafices the straight 256/3072bit encryption for a couple of 128bit keys and opens up access to everything that supports TLS1.0 (which is almost everything except IE6/8 on XP and old versions of Java).

This should give you the results 100/95/100/90. Like my site current has: (click to see the full report)

edgley.org - ssllabs.com results: 100/95/100/90

While this is not ideal, it's a much better solution than the majority of the (now outdated) "hardening Nginx" articles around, which all give between 80-90 on the "Key Exchange" and "Cipher Strength" ratings. Which, in my opinion, there isn't any excuse to have now. XP is dead, IE6-8 are dead. Java 6 and 7 are debatable, but I don't want my site to be vulnerable for the sake of supporting a language that is so full of security holes.