Optimizing HTTPS on Nginx

Now that you have secured Nginx with HTTPS and enabled SPDY enabled HTTP/2, it’s time to improve both the security and the performance of the server.

Nginx out-of-the-box is already performing quite well, and as far as I know, is the only web server with forward secrecy (FS) enabled by default (more on FS support in servers and clients here). There is still a few things we can configure to make Nginx faster and more secure.

NOTE: All of the configuration directives explained here will be for your server block in your Nginx config.

Step 1: Connection credentials caching

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 SPDY, 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 is specified in bytes (in this example: 20 MB). According to the Nginx documentation can 1MB store about 4000 sessions, so for this example, we can store about 80000 sessions, and we will store them for 180 minutes. If you expect more traffic, increase the cache size accordingly.

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.

Step 2: Disable SSL

– Say, what?

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):

Step 3: Optimizing the 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:


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?

Step 4: 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.

Step 5: Enable 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, similar to what we did when we first to enable HTTPS on Nginx (remember the order here is important):

cat PositiveSSLCA2.crt AddTrustExternalCARoot.crt > trustchain.crt

I am using a Positive SSL certificate so AddTrustExternalCARoot.crt is the root certificate and PositiveSSLCA2.crt is the intermediate. Replace with your issuer’s certificates accordingly. If you don’t have your CA’s root certificate, it should be available from their web site or you have to contact them.

Next, enable stapling and verification:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/cert/trustchain.crt;

You also need to provide one or more DNS servers for Nginx to use. Here I’m using Google’s public DNS servers, but you are free to use whichever works for you (if you don’t like Google or are worried about privacy, OpenDNS might be a good option for you). The resolvers are used in a round-robin fashion, so make sure all of them are good ones.

Step 6: Strict Transport Security

Even though you already should have made all regular HTTP requests redirect to HTTPS when you enabled SPDY, 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=31536000" always;

The max-age is set in seconds. 31536000 seconds is equivalent to 365 days.

If you want HSTS to apply to all subdomains, you use this config instead:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

That’s it.


I know how annoying it is to follow guides like this. You just want the config, right?
Well, here it is:

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        ssl_certificate /etc/nginx/cert/bjornjohansen.no.certchain.crt;
        ssl_certificate_key /etc/nginx/cert/bjornjohansen.no.key;

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

        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_protocols TLSv1 TLSv1.1 TLSv1.2;

        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /etc/nginx/cert/trustchain.crt;

        #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header Strict-Transport-Security "max-age=31536000" always;

        # Rest of your regular config goes here:
        # […]

Now that you’re done, go check your site on Qualsys SSL Labs test. You should have an «A+» rating.

Have fun, be safe, encrypt everything!

BTW: Now that you have HTTPS properly set up, it is time to look at HTTP Public Key Pinning (HPKP).

There are 40 comments

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.