HTTPS and HTTP/2 with letsencrypt on Debian and nginx

Introduction

Most web servers today run HTTP/1.1, but HTTP2 support is growing. I've written more in-depth about the advantages of HTTP/2. Most browsers which support HTTP2 only supports it over encrypted HTTPS connections. In this article, I'll go through setting up NGINX to serve pages over HTTPS and HTTP/2. I'll also talk a bit about tightening your HTTPS setup, to prevent common exploits.

Letsencrypt

HTTPS security utilises public-key cryptography to provide end-to-end encryption and authentication to connections. The certificates are signed by a certificate authority, which can then verify that the certificate holder is who he claims to be.

Letsencrypt is a free and automated certificate authority who provides free certificate signing, which has historically been a costly affair.

Signing and renewing certificates from Letsencrypt is done using their certbot tool, this tool is available in most package managers which mean it's easy to install. On Debian Jessie, just install the certbot like anything else from apt:

aptitude install certbot -t jessie-backports

Using certbot you can start generating new signed certificates for the domains of your hosted websites:

certbot certonly -w  -d 

For example

certbot certonly -w /var/www/example.com -d example.com

Letsencrypt certificates are valid for 90 days, after which they must be renewed by running

certbot renew

To automate this process, you can add this to your crontab, and make it run daily or weekly or whatever you prefer. In my setup it runs twice daily:

0 0,12 * * * certbot renew --quiet

Note that Letsencrypt currently doesn't support wildcard certificates, so if you're serving your website from both example.com and www.example.com (you probably shouldn't), you need to generate a certificate for each.

NGINX

NGINX is a free open source asynchronous HTTP server and reverse proxy. It's pretty easy to get up and running with Letsencrypt and HTTP/2, and it will be the focus of this guide.

HTTPS

To have NGINX serve encrypted date using our previously created Letsencrypt certificate we have to ask it to listen for HTTPS connections on port 443, and tell it where to find our certificates.

Open your site config, usually found in /etc/nginx/sites-available/<site>, here you'll probably see that NGINX is currently listening for HTTP-connections on port 80:

listen 80 example.com;

So to start listening on port 443 as well, we just add another line:

listen 443 ssl example.com;

The domain at the end would, of course, be the domain that your server is hosting.

Notice that we've added the ssl statement in there as well.

Next, we need to tell NGINX where to look for our new certificates, so it knows how to encrypt the data, we do this by adding

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

With the default Letsencrypt settings this would translate into something like:

ssl_certificate /example.com.crt;
ssl_certificate_key /example.com.key;

And that's really all there is to it.

HTTP/2

Enabling HTTP/2 support in NGINX is even easier, just add an http2 statement to each listen line in your site config. So:
listen 443 ssl example.com;

Turns into:

listen 443 ssl http2 example.com;

Now test out your new, slightly more secure, setup:

nginx -t

If all tests pass, restart NGINX to publish your changes.

service nginx restart

Now your website should also be available using the https:// scheme... unless the port is blocked in your firewall (that could happen to anybody). If your browser supports HTTP2, your website should also be served over this new protocol.

Improving HTTPS security

With the current setup, we're running over HTTPS, which is a good start, but a lot of exploits have been discovered in various parts of the implementation, so there are a few more things we can do to harden the security. We do this by adding some additional settings to our NGINX site config.

Firstly, old versions of TLS is insecure, so we should force the server to not revert:

ssl_protocols TLSv1.2;

If you need to support older browsers like IE10, you need to turn on older versions of TLS;

 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

This in effect only turns off SSL encryption, it's not optimal, but sometimes you need to strike a balance, and it's better than the NGINX default settings. You can see which browsers supports which TLS versions on caniuse.

When establishing a connection over HTTPS the server and the client negotiates which encryption cypher to use. This has been exploited in some cases, like in the BEAST exploit. To lessen the risks, we disable certain old insecure ciphers:

ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers On;

Normally HTTPS certificates are verified by the client contacting the certificate authority. We can turn this up a bit by having the server download the authority's response, and supply it to the client together with the certificate. This saves the client the roundtrip to the certificate authority, speeding up the process. This is called OCSP stapling and is easily enabled in NGINX:

ssl_trusted_certificate /etc/letsencrypt/live//chain.pem;
ssl_stapling on;
ssl_stapling_verify on;

Enabling HTTPS is all well and good, but if a man-in-the-middle (MitM) attack actually occurs, the perpetrator can decrypt the connection from the server, and relay it to the client over an unencrypted connection. This can't really be prevented, but it is possible to instruct the client that it should only accept encrypted connections from the domain; this will mitigate the problem anytime the clients visits the domain after the first time. This is called Strict Transport Security:

add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
BE CAREFUL! with adding this last setting, since it will prevent clients from connecting to your site for one full year if you decide to turn off HTTPS or have an error in your setup that causes HTTPS to fail.

Again, we test our setup:

nginx -t

If all tests pass, restart NGINX to publish your changes (again, consider if you're actually ready to enable Strict Transport Security).

service nginx restart

Now, to test the setup. First we run an SSL test, to test the security of our protocol and cipher choices, the changes mentioned here improved this domain to an A+ grade.

We can also check our HTTPS header security. Since I still havn't set up Content Security Policies and HTTP Public Key Pinning I'm sadly stuck down on a B grade, leaving room for improvement.

Further reading