HTTP/2 and SSL on nginx

HTTP is probably one of the most well known protocols out there. But did you know that a few years ago, it got it's first major revision since it's inception - HTTP/2.

One of the main goals was to speed up page load times (latency) by adopting some of the key features that were available in SPDY. The other one was requiring encryption. While the standard doesn't necessarily require HTTPS, most browsers will only do HTTP/2 over HTTPS.

Enabling HTTP/2 in nginx

As of nginx 1.9.5 is it super easy to enable HTTP/2 for your HTTPS server.

First, make sure you run a version of nginx that supports HTTP/2.

nginx -V

Double check to make sure that the output says: --with-http_v2_module.
If it doesn't, you'll need to upgrade to a newer version of nginx or compile it with the module.

Now, it's a single line change to enable HTTP/2. As mentioned above, currently all implementations require HTTPS, so find the https block of your server and simply change 1 line:

listen 443 ssl;

to:

listen 443 ssl http2;

and then reload your configuration:

service nginx reload

Now all HTTP/2 enabled visitors should get your site delivered over HTTP/2, while older browsers get regular SSL (really: TLS).

That's it for enabling HTTP/2 on nginx.

Next, up, we want to tweak the configuration to get that elusive A+ for SSL as all your traffic should now happen on HTTPS in order for it to leverage HTTP/2.

Tweak your nginx HTTPS configuration

nginx does perform quite well out of the box already, but you can always tweak your configuration a bit to get the perfect score.

SSL Session Cache

Almost all of the overhead with SSL/TLS is during the initial connection setup, so by caching the connection parameters for the session, will drastically improve subsequent requests (or in the case of HTTP/2, requests after the connection have closed – like a new page load).

All we need is these two lines:

ssl_session_cache shared:SSL:20m;
ssl_session_timeout 180m;

This will create a cache shared between all worker processes. The cache size in this example is set to 20MB. According to the Nginx documentation for every 1MB it can store about 4000 sessions, so in this example, we can store about 80000 sessions. We'll store them for 180 minutes.

I usually don’t recommend lowering the ssl_session_timeout to below 10 minutes, but if your resources are sparse and your analytics tells you otherwise, go ahead. Nginx is supposedly smart enough to not use up all your RAM on session cache, even if you set this value too high, anyways.

Disabling legacy SSL

Techically SSL (Secure Sockets Layer) is actually superseded by TLS (Transport Layer Security). I guess it is just out of old habit and convention we still talk about SSL.

SSL contains several weaknesses, there have been various attacks on implementations and it is vulnerable to certain protocol downgrade attacks.

The only browser or library still known to mankind that doesn't support TLS is of course IE 6. Since that browser is dead (should be, there is not one single excuse in the world), we can safely disable SSL.

The most recent version of TLS is 1.2, but there are still modern browsers and libraries that use TLS 1.0.

So, we’ll add this line to our config then:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

That was easy, now on to something more complicated (which I made easy for you):

Optimising Cipher Suites

The cipher suites are the hard core of SSL/TLS. This is where the encryption happens, and I will really not go into any of that here. All you need to know is that there are very secure suits, there are unsafe suites and if you thought browser compatibility issues were big on the front-end, this is a whole new ballgame. Researching what cipher suites to use, what not to use and in what order takes a huge amount of time to research. Luckily for you, I’ve done it.

First you need to configure Nginx to tell the client that we have a preferred order of available cipher suites:

ssl_prefer_server_ciphers on;

Next we have to provide the actual list of ciphers:

ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DHE+AES128:!ADH:!AECDH:!MD5;

All of these suites use forward secrecy, and the fast cipher AES is the preferred one. You’ll lose support for all versions of Internet Explorer on Windows XP. Who cares?

Generate DH parameters

If you want an explanation, read the DHE handshake and dhparam part on the Mozilla wiki. I’m not doing that here.

Create the DH parameters file with 2048 bit long safe prime:

openssl dhparam 2048 -out /etc/nginx/cert/dhparam.pem

And add it to your Nginx config:

ssl_dhparam /etc/nginx/cert/dhparam.pem;

Note that Java 6 doesn’t support DHParams with primes longer than 1024 bit. If that really matters to you, something is a bit wrong somewhere.

OCSP stapling

Online Certificate Status Protocol (OCSP) is a protocol for checking the revocation status of the presented certificate. When a proper browser is presented a certificate, it will contact the issuer of that certificate to check that it hasn’t been revoked. This, of course, adds overhead to the connection initialization and also presents a privacy issue involving a 3rd party.

Enter OCSP stapling:

The web server can at regular intervals, contact the certificate authority’s OCSP server to get a signed response and staple it on to the handshake when the connection is set up. This provides for a much more efficient connection initialization and keeps the 3rd party out of the way.

To make sure the response from the CA is not tampered with, we also set up Nginx to verify response using the CA’s root and the intermediate certificates.

Luckily, Lets Encrypt has a file of the full chain in the chain.pem file.

As such, just add the following to your server configuration:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/florianjensen.com/chain.pem;
resolver 8.8.8.8 8.8.4.4;

Obviously, use your own certificate filepath. As you also need to provide DNS servers for nginx to use, you may want to use the same as you already use on your machine. In the example above I'm simply using Google's DNS.

Strict Transport Security

Even though you already should have made all regular HTTP requests redirect to HTTPS when you enabled HTTP/2, you do want to enable Strict Transport Security (STS or HSTS) to avoid having to do those redirects. STS is a nifty little feature enabled in modern browsers. All the server does is to set the response header Strict-Transport-Security with a max-age value.

If the browser have seen this header, it will not try to contact the server over regular HTTP again for the given time period. It will actually interpret all requests to this hostname as HTTPS, no matter what. You can even tell the browser to enable the same behaviour on all subdomains. It will make MITM attacks with SSLstrip harder to do.

All you need is this little line in your config:

add_header Strict-Transport-Security "max-age=15552000" always;

The max age is set to 15552000 seconds, equivalent to 180 days which is the minimum required to get an A+ grade.

Wrap up

That's it! You should now have a nginx server running HTTP/2 and having a tweaked HTTPS configuration.

Here's a copy of my full server configuration:

server {
    listen       443 ssl http2;
    server_name  florianjensen.com;

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

    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 180m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DHE+AES128:!ADH:!AECDH:!MD5;

    ssl_dhparam /etc/nginx/cert/dhparam.pem;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/florianjensen.com/chain.pem;
    resolver 8.8.8.8 8.8.4.4;

    add_header Strict-Transport-Security "max-age=15552000" always;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        proxy_pass         http://docker-ghost-florianjensen;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto https;
        proxy_set_header   X-Forwarded-Host $server_name;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

Enjoy!