How To Securely Configure TLS and Generate Strong ECDHE Parameters On Your Nginx Server Without Sacrificing Compatibility

From the cheapest managed website hosting plan to the most high end rented server, if you plan to serve content over the internet, HTTPS is no longer optional.

As a result, pretty much every host out there will chuck in a free SSL certificate. Quick pedantic sidenote: although they're still very commonly referred to as SSL certificates, even by experts, no one actually uses the SSL protocol anymore because it's no longer secure. Today what we use is TLS.

Specifically, TLS 1.3 is the most recent version but lacks support in older clients, while TLS 1.2 is outdated and less secure but far more widely supported compared to 1.3, and by using a small list of good, secure ciphers you can avoid the exploits that have plagued TLS 1.2. Ideally, using only the latest version is best. In fact if you happen to be reading this a few years after publication you'll probably be fine dropping TLS 1.2.

But I'll stick to the here and now: 2021. By default, most web servers enable insecure protocols such as TLS 1 and TLS 1.1. These should not be used at all. They're inherently insecure.

In this post I will assume you either have a server you're setting Nginx up on, or already have an Nginx server up and running which you wish to upgrade the security of. I won't go over other important steps in securing a server such as firewall settings, SSH keys, auto updates and the like although I will cover these in a future article and in this one I will give you a few tips that can help prevent exploits just by changing your Nginx config.

So, here we go!

First up here is the base Nginx config I'll be using and walking you through. Note it's also commented so you can just skim through it and get an idea of what it all does, and if you copy/paste sections you won't forget what they're for later.

This config brings your server up to the latest TLS standards while keeping it compatible with older clients – even machines running Windows XP or phones running Android KitKat from back in 2014!

Nginx secure config template

# The following is baseline for a good, secure nginx web server config as of early 2021

# Listen on both IPv4 and IPv6 using SSL (TLS really) through HTTP2, declare the server name as your domain
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name mysite.tld;

 # Super secure TLS settings - these will get you A+ on SSL Labs, ensure strong encryption, and reduce attack surface by disabling unnecessary, insecure, outdated protocols
 # With TLS 1.2 enabled, compatibility is not sacrificed despite the limited ciphers
 # SSL Labs confirms these settings even work with clients as old as Windows XP (with Chrome or Firefox) and Android 4.4.2
 # If you are reading this a few years after publication, you may wish to disable TLS 1.2
  ssl_protocols TLSv1.3 TLSv1.2;
  ssl_ecdh_curve X25519:secp521r1:secp384r1;
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_prefer_server_ciphers on;
  resolver valid=60s;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 60m;

 # keepalive cache settings - the longer this is, the more compute power and bandwidth you save, but the bigger your cache becomes, always a delicate balance 
  keepalive_timeout 60;
  keepalive_requests 120;

  # TLS cert chain and private key, default Certbot paths
   ssl_certificate     /etc/letsencrypt/live/mysite.tld/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/mysite.tld/privkey.pem;
 # dhparams set up manually for secure ECDHE key exchange
   ssl_dhparam  /etc/private/dhparams.pem;

  # Security headers - HSTS, XSS protection, etc
  # I have not included a CSP as this must be specific to your use case, however if you're serving webpages I highly recommend looking up CSPs and deploying at least a basic one
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
   add_header X-Content-Type-Options "nosniff" always;
   add_header X-Xss-Protection "1; mode=block" always;
   add_header Referrer-Policy "strict-origin";
   add_header X-Frame-Options "DENY" always;

  # Hide the version of your web server and the OS it's running from public headers and error messages
   server_tokens off;

  # Point the server to where your site is stored and give it the name of the index page
   root /var/www/html/mysite;
   index index.html;
 # Limit the HTTP operations it accepts from clients to only what's necessary
 # Note that allowing GET also allows HEAD no need to call it separately
   location / {
   limit_except GET { deny all; }

# Help prevent buffer overflow attacks
 client_body_buffer_size 1K;
 client_header_buffer_size 1k;
 client_max_body_size 1k;
 large_client_header_buffers 2 1k;

# Redirect HTTP to HTTPS and redirect www to non-www
server {
    listen 80;
    listen [::]:80;
    server_name mysite.tld www.mysite.tld;
    server_tokens off;
    return 301 https://mysite.tld$request_uri;

As you can see, this is pretty comprehensive and explicitly ensures various settings – especially those related to TLS – are strong. They also make some assumptions – for example, that the site is purely static and there's no reason things need to be uploaded there. If you want users to be able to submit content, remember to add PUT to the limit_except line under location.

For the most part you can copy/paste the bits you want and they should work, just make sure they fit your site's use case. I recommend backing up your current config before messing with it.

There is only one step you need to perform manually to get this all working as it should: generate strong ECDHE (Ephemeral Elliptic-Curve Diffie-Hellman) parameters. It is possible that, if you've run Certbot and allowed it to configure Nginx for you, you already have a dhparams.pem file listed in your config.

Tell Certbot: “cert only!”

As an aside, I do not recommend allowing Certbot to configure Nginx for you, as Certbot sets its own insecure defaults and auto-renewal of your certificates can break if you simply try to set up TLS your own way.

This is quite easy to fix if you simply delete your current certs and generate new ones using DNS verification. This is also currently the only way to get a wildcard cert from Let's Encrypt, otherwise each subdomain needs its own certificate which is unnecessary.

For this reason I also highly recommend using Certbot with the --cert-only qualifier. This does what it says on the tin: generates certs, tells you where they are, and doesn't touch your server config, instead leaving you to do it yourself.

As you can see above, it's not difficult to manually point Nginx to your private key and public cert. If you have subdomains, having a wildcard cert also means you don't need to generate new certs for those subdomains anymore. You can have one wildcard, set it up universally, and delete any individual subdomain certs you may have.

If you want a wildcard cert, you need to use DNS challenges instead of the default well-known folder method. There is a very good article on how to set Certbot to get a Let's Encrypt wildcard certificate for your domain using DNS verification here. It's the method I use myself and it works great.

Setting up Ephemeral Elliptic-Curve Diffie-Hellman key exchange

What it doesn't do, however, is generate DH params. Luckily, you can generate secure ones yourself very easily without even installing any extra software.

First a word of warning. If you are using a cheap VPS with basic specs, it will be much quicker to generate this file on your computer and upload it to the VPS using SFTP. If your VPS is more beefy than the standard 1 vCPU, 1GB RAM type setup, you should be fine.

Generating and setting up dhparams.pem on your VPS

First thing's first, we want to store this file in a secure directory. I made my own one called /etc/private but you can put it wherever. I recommend not putting it in your home folder or in any Let's Encrypt directories in case it causes future problems.

sudo mkdir /etc/private && cd /etc/private
sudo openssl dhparam -out dhparams.pem 4096

At this point OpenSSL will begin generating safe primes and will warn you “this is going to take a long time” so sit back and chill for a bit, get a coffee or a spliff then come back when it's done.

Now for the even easier bit:

If it succeeded successfully, the end of the big long line of dots should be ++*++*++* and you're good to go. If there was some type of error or the VPS is hanging, it's possible it's simply not powerful enough. You can probably get it finished anyway by adding some swap, but it may be easier to do it on your own computer (that's next).

But assuming all looks good, simply run ls -l to confirm that the file is indeed there and to check the permissions. We gonna need to change these for security so only root can read them. Luckily a very simple process.

Simply sudo chmod 600 dhparams.pem then run ls -l again to confirm it worked. You should see this:

If so, you did it right.

Now go back to your Nginx config file and ensure it includes the TLS settings from above:

  ssl_protocols TLSv1.3 TLSv1.2;
  ssl_ecdh_curve X25519:secp521r1:secp384r1;
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_prefer_server_ciphers on;
  resolver valid=60s;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 60m;

  ssl_certificate     /etc/letsencrypt/live/mysite.tld/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/mysite.tld/privkey.pem;
  ssl_dhparam  /etc/private/dhparams.pem;

Make sure not to repeat any settings that are already there. If you've already put the rest, just add the ssl_dhparam line and make sure you've got the ssl_ecdh_curve settings there as well as the ssl_ciphers.

You're now done! Just make these settings active by restarting Nginx:

sudo nginx -s reload
sudo systemctl restart nginx

Why use both? The first alerts you of any errors that may exist and, if it finds any, no changes are made. The second fully restarts the service. I find when fiddling with DH I need to run both to make sure the settings are fully applied. Unsure why.

You can now confirm it works by typing your domain into SSL Labs and waiting for it to work its magic. It should list only a few highly rated ciphers for TLS 1.3 and 1.2 only. It should also show, on the list of emulated devices it's attempted to connect to your site with, one of the three curves in the config along with either “ECDHE” or “DH”. Any field that says “DH” should also say “4096 bits”.

Additionally you should notice some other good security settings. HSTS will be enabled. OCSP stapling will also be enabled. It'll confirm your server is not vulnerable to various exploits from outdated versions of TLS. And it'll confirm you aren't reusing DH params (because you literally just generated them). If you look at the details of the HTTP request it should show a few security headers and the server signature should only say “nginx” without a version number or OS.

At the time of writing, you will get a nice big green A+ if you copied all those TLS settings.

For further info, Immiweb's free SSL test highlights the DH keys and curves much more than SSL Labs so you can be sure it works as it should. In fact according to that site my humble little Nitter instance is PCI-DSS, HIPAA, and NIST compliant – meaning it's secure enough to accept credit card details, medical records, and secure government data!

Generating the dhparams.pem on your computer

If your VPS just can't hack it, never fear! If you have a Linux or Mac computer, or even a Windows computer with WSL installed, you can generate your own DH file on your local machine and upload it to your VPS via SFTP.

With a well spec'd computer, you shouldn't have to wait more than a minute. There's no timer but I generated the dhparams.pem file for one of my busier VPS's on my M1 MacBook Air and it did it in the space of a tiny terminal window, I estimate slightly over 30 seconds. By contrast, using the VPS it took almost 10 minutes.

The steps are mostly the same except you're running the command on your own computer. Change to whatever directory you want it in, I just kept it in my home folder to keep things simple and ran that same command:

openssl dhparam -out dhparams.pem 4096

Afterwards, instead of SSHing into the VPS, I SFTP'd in. This is exactly the same process in the terminal as SSH except you just use the sftp command instead of ssh like so:

sftp username@169.420.420.1

Now, assuming you put your dhparams file in your home folder, all you need to do is run:

put dhparams.pem

If you put it somewhere else, first run:

lcd /path/to/the/folder

Then the same put command as above.

(lcd is “local change directory” for those unfamiliar – if you enter cd in an SFTP session it will change the directory on the remote machine.)

Once that's done (transfer should be almost instant) type exit and log back into a regular SSH session.

Now you should be able to see the file if you run ls so let's make the same folder as we did in the previous instructions and move the params over:

sudo mkdir /etc/private
mv dhparams.pem /etc/private
cd /etc/private
sudo chmod 600 dhparams.pem
ls -l

As long as that final command shows the correct permissions, you're done!

Now as above just make sure the correct lines are in your Nginx config, reload Nginx, and you're good to go!

You should now run the same tests listed above – SSL Labs and Immuniweb – to confirm everything is working as you configured it.

Final thoughts

Overall, if you already have a site running, you most likely just want to take bits and pieces of this relevant to your use case. But keeping your security up to date is always vital.

These settings keep your site secure while still allowing it to be accessible by even Windows XP users. Once it becomes recommended to drop TLS 1.2 altogether, compatibility for older clients will begin to diminish, but at the time of writing there's no drawbacks to keeping things up to the latest security standards and it's really not very difficult either.

If, like me, you often have multiple vhosts running under /etc/nginx/sites-enabled it's a good idea to take the parts you'll want on all of them and add them to /etc/nginx/default, for example the TLS settings defining the protocols and ciphers. This will ensure every site is using a secure TLS config.

Depending on your setup, you can include the certificates in the default config too. If your vhosts are all subdomains and you have a wildcard TLS certificate, this is another easy way to avoid having to set up and update configs in multiple places.

Of course, the settings you put in the individual vhosts override the defaults, so if one vhost is a different domain you can put different certs in there without affecting anything else, or if you want all vhosts to be uniform you should make sure they don't contain their own settings for whatever you put in default.

Next up: how to use Nginx as a reverse proxy for an S3 bucket! You want the flexibility and cheapness of S3 static site hosting without having to use Cloudfront? Or maybe you simply want to host images and other media for your main site in S3, and again, don't want to pay for and/or bother with Cloudfront. Keep an eye out!

#VPS #TLS #SSL #HTTPS #HSTS #ECDHE #Nginx #Linux #Ubuntu #server #tutorial #guide