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/www.bjornjohansen.com.certchain.crt;
        ssl_certificate_key /etc/nginx/cert/www.bjornjohansen.com.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).


  1. Hello sir,

    I’ve been following your guide to optimize https, and the only thing I have left is the OSCP stapling.

    I have a Positive SSL from comodo, they on their website I’ve found a list that I think is the order of the chain.

    UserTrust / AddTrust External Root
    COMODO RSA Certification Authority
    COMODO RSA Domain Validation Secure Server CA
    End-Entity/Domain Certificate

    In your example you have a cert called trustchain.crt .
    Is that generated using openssl ? In other examples I’ve seen a .pem file.

    I have all the files from the list, I’m stuck with with the trustchain.crt file.
    By the way, I made my server finally work, I’m using nginx, mysql and hhvm, http://alexhera.me ( it’s empty, but http2 capable) :)

    Best regards,

  2. Update to previous post!

    I finally did it, it seams that for Comodo users at least it gives an error when combining multiple certs, I used alexhera_me.crt comodo-rsa-domain-validation-sha-2-intermediates.ca-bundle >> ssl-bundle.crt

    and added it to the line ssl_certificate

    Then the AddTrustExternalCARoot.crt like in your example in ssl_trusted_certificate.

    With some minor tweaks I made it. :)


  3. I believe you have the required order of certificates in the chain for stapling reversed.

  4. Wow, it’s amazing how much I’ve learned with this (comparatively old) article of yours in just 15 minutes! Thank you so much for writing this, as well as the detailed explanations of what it actually does — something which often is missing on tutorials like these.

    I also left OSCP stapling out of the configuration, at least for now, because almost all sites I manage have CloudFlare in front of them — meaning that they don’t have ‘real’ certificates, but merely establish a secure connection to CloudFlare. CloudFlare seems to have their configuration rather well done, though, since they certainly get the A+ (and have OSCP stapling enabled) when I test my website’s address at the Qualsys SSL Labs test.

    Well done, thank you!

  5. Hi,

    Ich followed your guide, but I my tests fail at the add_header line. It says “invalid number of arguments in add_header directive” and I don’t get why. Any Ideas?

  6. Old versions of nginx as the one in Jessie don’t support the always parameter in the add-header line for hsts.

  7. Thank you for the great guide. I just implemented this on my site https://voiledombragefrance.fr/ and got an A+ rating ! I consulted a number of other on line guides but when I checked their own sites the Qualys rating was low – yours is still holding A+.
    In the article you have not mentioned submitting your site to https://hstspreload.appspot.com/ to get it added to the browser preload list. This requires a slight change to your HSTS code (adding preload):
    Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
    They also recommend applying HSTS in stages, gradually increasing max-age and checking all is OK before making the max-age well into the future.

  8. I am loving your posts. I got a B grade for my ssl sites and decided to generate custom DH parameters to make sure that my sites always use 2048 bits. I saw permission denied errors and the process takes a long time to complete so I ended up issuing this command: nohup openssl dhparam 2048 -out dhparam.pem &
    This runs the task in the background and writes the file to your user work area without any permission denied stuff.

    1. Thank you, Andrew. Yes, I usually do the dhparam generation in a separate shell or a background process while I’m doing other things too :-)

  9. Dude… you ROCK. I’ve never setup SSL before, and needed to do it on my nginx server at Linode. Your instructions worked flawlessly, and I now have an A+ rating. Can’t say thanks enough.

    1. Thank you for your comment :-)
      Your next steps are now HPKP and CSP, but be careful and don’t experiment on a domain you care about. If you mess things up, your site might be broken for months (especially with HPKP).

  10. Thanks for the great nginx insight.

    You might want to update this post as TLSv1 is generally acknowledged as insecure. Some even believe that TLSv1.1 should not be used.

    1. Do you have any reliable sources for that? AFAIK, both TLSv1 and TLSv1.1 meets today’s general security requirements and are necessary for client compatibility. You might have special needs, of course, but for those cases I recommend you consult a cryptographer, not a blogger.

  11. For what it’s worth I read the same about TLSv1.0 and TLSv1.1 on wiki. https://en.wikipedia.org/wiki/Transport_Layer_Security#Standards
    Obsolete.. v1.1 is hardly ever implemented without v1.2 being available too. So just use TLSv1.2 and loose a couple of really ancient browsers. When security is important having people update their browser/OS/device every once and a while is not that bad.

    Anyway thanx for the article! It helped me get an A-rating, there is an aweful lot of interconnected servers and we first want to test all to be well before we can set HSTS and HPKP…. =/

    1. When TLSv1.3 is finished, TLSv1.2 will be considered obsolete too.

      I’m not saying that either TLSv1.0 or TLSv1.1 are secure, but AFAIK they are secure enough for general usage. If tight security is more important than general availability, then by all means, disable everything but TLSv1.2. And hire a security firm you can trust.

  12. Hi,
    I am fairly new to nginx and I just want you to specify in which conf file all this goes : nginx.conf – sites-available default or sites-enabled default.
    Otherwise, it look really great and lloking forward to make it work for my set up.

    Thanks for your understanding,

    Guy Durand

    1. The actual file should be in sites-available, which you then symlink to sites-enabled like this:

      ln -s /etc/nginx/sites-available/yoursite.conf /etc/nginx/sites-enabled/yoursite.conf

      1. Depends on your distribution. Looks like you are using one (of the many) that uses the Apache-style directory layout.

  13. Hey there, excellent post! We’re trying to optimize our Nginx-EasyEngine powered SSL WWW website by following the above. however, we’re having trouble with the speed of loading for our static resources:
    If you look at the Waterfall you can see that dat-menu.css seems to be taking a long time to load. Can you help, please?

  14. Hi,

    Very nice explanation..

    But in step 3 there is an ; missing.

    This is:

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

  15. in the wrap up. the DH+3DES [which is not elsewhere], should be removed . Sweet32 . CVE-2016-2183. It is omitted elsewhere in the tutorial. Just wanted to make note, it came up in an openvas scan but still scores A+ on qualys with the 3DES. Thank you for your wonderful tutorials. it made converting to nginx after using apache since its birth easy.

    1. Yeah, DH+3DES shouldn’t be there. It might have been left there in an old version which still was cached in Varnish. I purged Varnish now, and it should be gone :-)

  16. Hei!

    As far as I understood, when using Let’s Encrypt, this would be the correct setup for the three certificates – or is there a better way?

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


  17. Hi Bjorn
    Excellent guide, thanks for all the generous advice on your site!

    I used your setup in my nginx server block, only then realising I had skipped your tip on using a release of nginx with http2. Reverting to listen 443 ssl, all seemed ok but without getting a particularly good rating on the Qualsys SSL Lab site. Nginx is part of a LEMP server running wordpress. And as you’ve guessed already, I’m pretty much a total newbie!

    Long story short, I have installed nginx v1.13.6 with the http_v2_module. Using the same nginx server block as before, my site does not load. Using https I get a ERR_CONNECTION_REFUSED message, and using http I get the “Welcome to nginx!” page. Thinking it could be at the root of the issue, I have reinstalled php but it does not seem to have changed anything.

    Given there are no similar issues posted online, I expect I’m have not done something basic to ensure the various parts of my LEMP stack are properly set up, following my reinstall of nginx. I’d be very grateful indeed if you have any advice on troubleshooting. Thanks!

  18. A few years ago I saw your article, and I use some settings in my nginx. Today, I noticed that the line:

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

    on my nginx server, prevents it from working on the cloudfront, where the https://xxxx.cloudfront.net address does not work in google chrome, displaying the ERR_SPDY_PROTOCOL_ERROR error.
    Even if the cloudfront origin is done on the 80 port, and not through port 443.

    I am not understanding if this is normal operation (no other domain / subdomain displays the content of my site by https), or if it is cloudfront problem.

    1. Hi Luciano

      First of all, if you’re adding HSTS, your resources should always be available on HTTPS. Make sure they are.
      Then, there’s no reason to use port 80 (HTTP) as origin for CloudFront. Use 443 (HTTPS).

      Second, the error ERR_SPDY_PROTOCOL_ERROR points to your server being configured to use SPDY. That was deprecated a long time ago. You should use HTTP/2 now. Replacing spdy with http2 on your listen directive line in your Nginx config. See: https://www.bjornjohansen.com/enable-http2-on-nginx

      1. Thank Bjørn Johansen,

        I checked all the settings, and all were ok, doors, etc. Reminiscing: it was running on firefox, and it was not running on the chrome or the cloudfront. The mistake? The line below had a line break, and some spaces at the end. So, it made me work for hours until I spot the error.

        I was almost screaming for help.

        *The sign of parentheses below are just to evidence the whitespace

        This does not work (Error ONLY if you enable http2.If you are using ssl by http1.1 it works)

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

        It works:

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

        I must be an idiot.

  19. With “openssl version” = “OpenSSL 1.1.0f 25 May 2017”
    on debian stretch the command:

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

    prints out the params on the console. If you put the 2048 at the end it works as expected:

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

    1. Same for me.

      1) BUT:

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

      gave me the following outcome:

      “Can’t open /etc/nginx/cert/dhparam.pem for writing, No such file or directory”

      But on the otherhand, this worked fine:

      “openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048”

      First of all, I noticed that there is no subfolder “/etc/nginx/cert” existing. I did not want to mess up with nginx subfolders…

      2) Now, the other thing is, I put

      ssl_dhparam /etc/ssl/certs/dhparam.pem;

      in the nginx.conf, HTTP Block. Quick test with “nginx -t” proved all find. But is this really the case? I mean doesn’t “ssl_dhparam” has to be put in the SERVER block?

  20. Hi Bjorn,

    Your website has been very helpful with configuring Nginx. I do have a question that I can’t seem to find an answer for. I am using google cloud load balancer and Nginx as the backend service. I want to redirect all requests to https and www. What I have always tried doesn’t effect the https://ampkart.com. It does not redirect it to https://www.ampkart.com, it remains the Same.

  21. I am a new developer and just recently started managing my own VPS and it was my 2 time setting up SSL for my site and as I am a self taught person, So I use to do setup the SSL only in config and thought its done but today I encountered your post and learned so many thing. Thankyou for you posts and they are still relevant and working. I was just wondering that {ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DHE+AES128:!ADH:!AECDH:!MD5;} is still up-to-date or there should be some update.
    Thank you.

Comments are closed.