Moving to TLSv1.2 or newer: Nginx, Apache, Postfix, Dovecot

Moving on from “should we do it?” (with the answer to most real-world scenarios being “yes, and as a bonus it can help block a lot of spambots“), here’s how to restrict several Internet services — Nginx, Apache, Postfix, and Dovecot — to TLSv1.2 or newer.

As usual, these are not complete guides for any of those servers; I’m assuming you already have them working fine (including TLS encryption), and just want to disable any TLS protocols lower than v1.2. (If you need to add TLS to a non-TLS server, see instructions for Nginx, Apache, Postfix, and Dovecot.)

Nginx:

In each virtual host’s server section — or, even better, if you’re using Let’s Encrypt, in /etc/letsencrypt/options-ssl-nginx.conf or its equivalent –, add the following (or replace any existing ssl_protocols entry):

ssl_protocols TLSv1.2 TLSv1.3;

Restart Nginx, and test it on SSL Labs. You should get something like this1:

TLS v1.2 and v1.3 only

(Note: if you’re using a very old version of Nginx, it may not accept the “TLSv1.3” parameter and refuse to start; in such a case, remove it — or, better yet, upgrade your system. 🙂 )

Apache:

Similarly to Nginx, you can add one of the following to each Virtual Host, or to the global HTTPS configuration (typically in /etc/httpd/conf.d/ssl.conf), or, if using Let’s Encrypt, to /etc/letsencrypt/options-ssl-apache.conf :

SSLProtocol            +TLSv1.2 +TLSv1.3

or:

SSLProtocol             all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1

Right now, they’ll do the same thing: allow TLSv1.2 and v1.3 only. Personally, I like the second version (which disables older protocols) better, for two reasons: 1) it’ll work even with some ancient Apache version that doesn’t recognize “TLSv1.3”, and 2) when future TLS versions are added, they’ll be enabled, making it more future-proof.

Again, you can test the new configuration on SSL Labs.

Postfix:

Add the following to /etc/postfix/main.cf (replacing any equivalent entries, if they exist).

smtp_tls_mandatory_protocols=!SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols=!SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols=!SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols=!SSLv2, !SSLv3, !TLSv1, !TLSv1.1

After restarting Postfix, you can test its available protocols with Immuniweb’s SSL Security Test (specify something like yourhostname:25, or yourhostname:465 if not using STARTTLS).

Dovecot:

Add the following (or replace it if it exists) to your SSL configuration (typically /etc/dovecot/conf.d/10-ssl.conf):

ssl_min_protocol = TLSv1.2

Restart Dovecot, and test it on Immuniweb’s test (use something like yourhostname:143 , or yourhostname:993 if not using STARTTLS).

Moving to TLSv1.2 or newer: is now the time?

TLSv1.0, used not only for HTTPS but also for secure SMTP, IMAP, etc., has been around for a while — 1999, in fact –, and, while not “hopelessly broken” like SSL 2 and 3 are, there have been many successful attacks/exploits against it in the past two decades, and while most implementations (in terms of both servers and clients) have been patched to work around those, there may always be another one just around the corner. Also, TLSv1.0 came from a time where it was desirable to support as many different ciphers as possible, which means that, by default, some ciphers later found to be insecure may still be enabled.

Therefore, the end of TLSv1.0 has been talked about for a while, and, indeed, the main browser vendors — Google, Mozilla, Apple, and Microsoft — have announced that they’ll deprecate TLSv1.0 (and 1.1, which is little more than a revision of 1.0) by early 2020.

Now, until a few days ago I still had TLSv1.0 enabled on my servers (including Nginx, Postfix and Dovecot); my belief then being that some encryption was always better than no encryption, and besides we were mostly talking about public websites, that didn’t ask for user credentials or anything. Furthermore, any modern browsers/email clients default to secure protocols (TLSv1.2 or better), so there would never be, “in real life”, a situation where important/sensitive data (say, me logging on to my own mail server, or to one of my WordPresses) was being transmitted in an insecure way.

On the other hand… doesn’t the above mean that, “in real life”, nobody will really be using TLSv1.0 “legitimately”? Doesn’t it mean that setting up TLSv1.2 as a minimum will have no adverse consequences?

So I investigated it a little:

  • in terms of browsers/clients, requiring TLSv1.2 or above means: 1) no Internet Explorer before version 11; 2) no Safari before 6.x; 3) no default Android browser before Android 4.4.x (but those users can still use Chrome or other non-default browsers). (You can check this out on SSL Labs). I don’t have a way to check email clients that old, but you can assume that anything released after the browsers just mentioned will be OK;
  • in terms of my own server logs (mostly Nginx, but also Postfix, in terms of opportunistic encryption), apparently, only spambots and such are using TLSv1.0; any “legitimate” email servers use TLSv1.2 or 1.3. The same thing with my websites — any TLSv1.0 entries in my logs are always from some bot (and not one for a “well-known” search engine), not actual visitors. 1

In short: you can probably move to requiring TLSv1.2 or above now (and, indeed, I’ve done so since a few days ago), unless you’re in some very peculiar situation — maybe your site needs to be accessed by users of a company whose IT policy includes not using software newer than 20 years old? Or you have a CEO who’s always refused to upgrade his beloved Symbian phone, but who’d flip out if he couldn’t access your company’s webmail with it? 🙂 Otherwise, my advice is: go TLSv1.2 and don’t look back.

However, and since this post is already quite long, I’ll leave the specific instructions for disabling TLS before 1.2 in Nginx, Apache, Postfix, and Dovecot for the next one

Nginx: How to prioritize ChaCha20 for devices without hardware AES support

Some time ago, I noticed something like this on a site (not mine)’s rating on SSL Labs:

SSL Labs - "P"

According to SSL Labs’ test, that little P means:

(P) This server prefers ChaCha20 suites with clients that don’t have AES-NI (e.g., Android devices)

Naturally, my curiosity was piqued, and a bit of investigating followed…

First (and briefly), the theory:

  1. both AES and ChaCha20 ciphers are thought to be equally secure.
  2. many modern CPUs provide hardware acceleration for AES, in which case it’s faster…
  3. … however, if the CPU doesn’t accelerate AES, then ChaCha20 is faster.

Therefore, setting a web server (or any other kind of server, such as IMAP or SMTP, but those are outside this article’s scope) to use either option all the time means that some clients won’t have the optimal cipher(s) (in terms of performance) for their device. It won’t probably be noticeable in 99% of cases, but, hey, I’m a geek, and geeks optimize stuff for fun, not just for real-world performance. 🙂

Fortunately, as the SSL Labs test shows, it’s possible to configure a server to use/prefer one cipher for non-hardware accelerated devices, and another cipher (or, more precisely, a list of preferred ciphers) for the rest.

(For the curious, the selection process works like this: if the client’s preferred cipher is ChaCha20, then the server assumes it’s a device without hardware AES and uses that. If the client’s top cipher is anything else, then the server uses its own cipher list.)

This guide shows how to do it in Nginx, using Let’s Encrypt certificates. I’m using an Ubuntu 19.04 server, but any relatively recent Nginx should work, as long as you’re using OpenSSL 1.1.0 or newer, or LibreSSL. If your distro still provides only OpenSSL 1.0.x or older by default, you can always compile a newer version to a separate directory, and then compile Nginx to use it: here are instructions for OpenSSL 1.1.x or LibreSSL.

This is actually relatively simple, though some specific distros/servers may require some other changes:

1- Give Nginx a list of ciphers that doesn’t have ChaCha20 in first place (you need to specify some AES ciphers first). The list I’m using (currently specified in /etc/letsencrypt/options-ssl-nginx.conf , since I’m using Let’s Encrypt) is:

ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA::!DSS:!3DES';

2- Edit your OpenSSL/LibreSSL configuration file (in Ubuntu/Debian it’s /etc/ssl/openssl.cnf) and, just before the first section in square brackets (in Ubuntu it’s “[ new_oids ]“), add the following:

openssl_conf = default_conf

[default_conf]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
Options = ServerPreference,PrioritizeChaCha

3- Restart Nginx, and test it on SSL Labs. If you see the superscript “P” at the right of each ChaCha20 line, everything went well. (Problems? Ask here, and maybe I, or some other reader, can help.)

You can also set up your Nginx to log ciphers (possibly to a separate log file), using something like:

log_format logciphers '$remote_addr ' '$ssl_protocol/$ssl_cipher ' '"$request" $status $body_bytes_sent ' '"$http_user_agent"';

access_log /var/log/nginx/servername-ciphers.log logciphers;

And then access the site with some of your devices, noting what ciphers they use. For instance, both my (2015) i7 laptop and my (2016) ZenFone 3 Android phone use AES (yes, apparently the Snapdragon 625 chip includes AES acceleration), but a 2013 Nexus 7 tablet and a 2015 iPad Pro (Safari, in this case — all other examples used Chrome) went for ChaCha. This shows that everything’s working correctly — the devices that specifically want to use ChaCha are indeed using it, while the rest aren’t. And, yes, this means that the very same browser (in this case, Chrome on Android) has different cipher preference defaults depending on the hardware it’s running on.

How to get a 100% score on SSL Labs (Nginx, Let’s Encrypt)

This post should be titled “How to get a 100% score on SSL Labs (Nginx, Let’s Encrypt) as of April 2018“, since SSL Labs‘s test evolves all the time (and a good thing it does, too.) But that would be too long a title. 🙂

The challenge:

As we’ve seen before, it’s relatively easy to get an A+ rating on SSL Labs. However, even that configuration will only give a score like this:

Zurgl.com's current SSL Labs score
Zurgl.com’s current SSL Labs score

Certainly more than good enough (and, incidentally, almost certainly much better than whatever home banking site you use…), but your weirdo of a host can’t look at those less-than-100% bars and not see a challenge. 🙂

First, note that all of the following will be just for fun, as the settings to get 100% will demand recent browsers (and, in some cases, recent operating systems/devices), so you probably don’t want this in a world-accessible site. It would be more applicable, say, for a webmail used by you alone, or by half a dozen people. But the point here isn’t real-world use, it’s fun (OK, OK, and learning something new). So, let’s begin.

Getting that perfect SSL Labs score:

For this example, I’ll be using a just-installed Debian 9 server, in which I did something like:

apt -y install nginx
echo "deb http://ftp.debian.org/debian stretch-backports main" >> /etc/apt/sources.list.d/stretch-backports.list # if you don't have this repository active already
apt -y install python-certbot-nginx -t stretch-backports

(The reason for the above extra complexity is the fact that the default certbot in Debian Stretch is too old and tries to authenticate new certificates in an obsolete way, but there’s a newer one in stretch-backports.)

I also created a simple index.html file on /var/www/html/ that just says “Hello world!”. Right now, the site doesn’t even have HTTPS.

So, let’s create a new certificate and configure the HTTPS server in Nginx:

certbot --nginx --rsa-key-size 4096 --no-redirect --staple-ocsp -d mysite.mydomain.com

(replacing mysite.mydomain.com, obviously.)

Testing it on SSL Labs, it gives… exactly the same bars as in the image above, but with an A rating instead of A+. Right, we need HSTS to get A+ (and currently certbot doesn’t support configuring it automatically in Nginx), so we add this to the virtual host’s server section:

add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";

(Note: remove “includeSubDomains;” if you have HTTP sites on any subdomain. Also, HSTS makes your site HTTPS-only, so you can’t use it if you want to keep an HTTP version — in which case you can’t get better than an A rating.)

This upgrades the rating to A+, as expected, but the bars still didn’t change. Let’s make them grow, shall we?

Certificate:

It’s already at 100%, so yay. 🙂

Protocol Support:

Just disable any TLS lower than 1.2. In this case, edit /etc/letsencrypt/options-ssl-nginx.conf and replace the ssl_protocols line (or comment it and add a new one) with:

ssl_protocols TLSv1.2;

WARNING: any change you make to the file above will affect all virtual hosts on this Nginx server. If you need some of them to support older protocols, it’s better to just comment out that option in that file, and then include it in each virtual host’s server section.

EDIT: actually, it turns out that ssl_protocols affects the entire server, with the first option Nginx finds  (first in /etc/nginx.conf, then in the default website (which possibly includes /etc/letsencrypt/options-ssl-nginx.conf)) taking precedence. So just choose whether you want TLS 1.0 to 1.2 (maximum compatibility) or just 1.2 only (maximum security/rating); if you need both, use two separate servers.

Key Exchange:

We’ve already created the new certificate with a 4096-bit key (with “rsa-key-size 4096“, see above), and according to SSL Labs’ docs this should be enough… but it isn’t. After some googling, I found out that you also need to add the following option inside the server section in Nginx:

ssl_ecdh_curve secp384r1;

Since the default Nginx+OpenSSL/LibreSSL setting, either “X25519” or “secp256r1” (actually “prime256v1“), also lowers the score.

EDIT: again, ssl_ecdh_curve affects the entire server, so you can’t use different default curves for each virtual host. So I’d suggest (in /etc/letsencrypt/options-ssl-nginx.conf):

ssl_ecdh_curve secp384r1:X25519:prime256v1;

if you want the 100% rating, or:

ssl_ecdh_curve X25519:prime256v1:secp384r1:

otherwise. This one doesn’t affect compatibility, by the way; it’s just a question of the preferred order.

The certificate’s key size (4096 or 2048) is, like the certificate itself, specific to each virtual host.

Cipher Strength:

For 100% here, you need to disable not only any old protocols, but also any 128-bit ciphers. I started from Mozilla’s SSL tool (with the “Modern” option selected), then removed anything with “128” in its name, then moved ChaCha20 to the front just because, and ended up with this line, which I added to /etc/letsencrypt/options-ssl-nginx.conf (replacing the current setting with the same name):

ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384::ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384';

(This time, this setting can indeed be specified for each virtual host, though you’d need to comment it out in the file above (which affects all virtual hosts with Let’s Encrypt certificates) if you want to do so, otherwise they’ll conflict.)

The result:

100% on SSL Labs

As said near the beginning, this is not something you likely want to do (yet) for a production, world-accessible site, as it will require relatively modern browsers, and you typically can’t control what visitors use1. According to SSL Labs’ list, it’s not actually that bad: all versions of Chrome, Firefox, or Safari from the past couple of years are fine, but no Microsoft browser older than IE11 will work, nor will Android’s default browser before 7.0 (Chrome on Android, which is not the same thing, will do fine.) I’d suggest it for a site where you can “control” the users, such as a small or medium-sized company’s webmail or employee portal (where you, as a sysadmin, can and should demand up-to-date security from your users).

But the main point of this exercise was, of course, to see if I could do it. Challenging yourself is always good. 🙂 And if you can share what you learned with others, so much the better.

Red Hat/CentOS: How to add HTTPS to an existing Nginx website (both with and without Let’s Encrypt)

(Yes, you read the title correctly. For extra fun, and to prevent this blog from being too focused on Ubuntu/Debian, this time I’ll be using Red Hat Enterprise Linux / CentOS (and, I assume, Fedora as well.) Later on, I may post a Debian-based version.)

Configuring a basic HTTP site on Nginx

(Note: if you already have a working HTTP site, you can skip to the next section (“Adding encryption…”))

Yes, the post title mentions an “existing” website, which I believe will be the case in most “real world” situations, but installing a new one is actually very easy on CentOS1. First, do:

yum -y install epel-release; yum -y install nginx

Then create a very basic configuration file for the (non-HTTPS) site, as /etc/nginx/conf.d/mysite.conf :

server {
        listen 80;
        server_name mysite.mydomain.com;
        root /var/www/mysite;

        location / {
        }
}

Then, of course, create the /var/www/mysite directory (CentOS doesn’t use /var/www by default, but I’m far too used to it to change. 🙂 ) If you’d like, create an index.html text file in that directory, restart nginx (“service nginx restart” or “systemctl restart nginx“, depending on your system’s major version), and try browsing to http://mysite.mydomain.com . If it works, congratulations, you have a running web server and a basic site.

Adding encryption to the site (not using Let’s Encrypt):

First, you need a key/certificate pair. I have tutorials for creating an RSA certificate or an ECDSA certificate.

Second, edit the site’s configuration file (in the “starting from scratch” example above, it’s “/etc/nginx/conf.d/mysite.conf“), and copy the entire server section so that it appears twice on that text file (one after the other). Pick either the original or the copy (not both!), and, inside it, change the line:

listen 80;

to:

listen 443 ssl http2;

(Note: the “http2” option is only available in Nginx 1.9.5 or newer. If your version complains about it, just remove it, or upgrade.)

Inside that section, add the following options:

ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/privatekey.crt;

(replacing the paths and file names, of course.)

This should be enough — restart Nginx and you should have an HTTPS site as well as the HTTP one.

And what if you want to disable HTTP for that site and use HTTPS only? Just edit the same configuration file, look for the server section you didn’t change (the one that still includes “listen 80;“), and replace the inside of that section with:

listen 80;
server_name mysite.domain.com;
return 301 https://mysite.domain.com$request_uri;

Afterward, while you’re at it, why not go for an A+ rating on SSL Labs (skip the certification creation part from that tutorial, you’ve done it already) and an A rating on securityheaders.io?

Adding encryption to the site (using Let’s Encrypt):

First, install certbot:

yum install python2-certbot-nginx

Then check if Nginx is running and your normal HTTP site is online (it’ll be needed in a short while, to activate the certificate). If so, then enter:

certbot --nginx --rsa-key-size 4096 -d mysite.mydomain.com

(replacing “mysite.mydomain.com” with yours, of course.)

Answer the questions it asks you: a contact email, whether you agree with the terms (you need to say yes to this one), if you want to share your email with the EFF, and finally if you want “No redirect” (i.e. keep the HTTP site) or “Redirect” (make your site HTTPS only).

And that’s it (almost — see the next paragraph) — when you get the shell prompt back, certbot will already have reconfigured Nginx in the way you chose in the paragraph above, and restarted it so that it’s running the new configuration. You may want to add “http2” to the “listen 443 ssl;” line in the configuration file (it’ll probably be the default someday, but as of this post’s date it isn’t), and don’t forget your options for improved security and security headers.

Only one thing is missing: automatically renewing certificates. Strangely, the certbot package configures that automatically on Ubuntu, but not on CentOS, from what I’ve seen (please correct me if I’m wrong). The official Let’s Encrypt docs recommend adding this (which includes some randomization so that entire timezones don’t attempt to renew their certificates at precisely the same time) to root’s crontab:

0 0,12 * * * python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew 

(Note: It’s possible to use Let’s Encrypt to create ECDSA certificates, but as of this writing you have to do most of the work manually (creating a CSR, etc.), and you lose the automatic renewal, so for the moment I suggest using RSA certificates. I hope this changes in the future.)

How to configure TLS encryption in Dovecot

DovecotAs with most other internet services, Dovecot can be configured to use TLS encryption — and, unlike some others (such as web servers or SMTP servers), there’s little reason not to enforce it.

Notes:

  • Like other recent TLS tutorials on this blog, this is, of course, not a full Dovecot guide — that would be far too complex for a blog post. Instead, it’s just focused on enabling/configuring TLS encryption. I’ll be assuming you already have Dovecot configured, and are able to access mailboxes on your server using an email client.
  • I’ll be focusing on IMAP only, not POP3 or anything else.
  • The server will be configured to require encryption, for both privacy and security reasons. I don’t know of any modern email client that doesn’t allow encrypted connections (and even then, you might work around it by configuring an encrypted tunnel, but that falls out of this post’s scope).

1. Getting a certificate

While I believe most modern email clients will support ECDSA certificates, such clients are not a known quantity like, say, web browsers are, so I suggest that you create an RSA certificate, unless you already have one whose Common Name (CN) matches your server’s public name (e.g. mail.domain.com). Dovecot versions 2.2.31 and newer support configuring alternative certificates so that you could support both kinds at the same time, but there’s probably little gain in doing that, since most clients will likely default to RSA anyway (and, unless you’re using Let’s Encrypt, certificates are not free).

So, put your certificate and private key in /etc/dovecot/. Let’s assume the certificate is called myserver-full.crt 1, and the private key is myserver.key. You should protect the private key from any users on your server, so do, for instance,

chown root:dovecot /etc/dovecot/myserver.key
chmod 640 /etc/dovecot/myserver.key

2. Configuring Dovecot for TLS

I’m assuming your Dovecot installation has a basic configuration file in /etc/dovecot/dovecot.conf, but the real “meat” of the configuration is in included files in /etc/dovecot/conf.d/. Your system may be slightly different, but I’m sure you can adapt. 🙂 Also, the numbers at the beginning of the file names may differ in your system.

This one isn’t really related to TLS, but it’s a good idea: edit /etc/dovecot/conf.d/20-imap.conf, and look for a line like this:

mail_plugins = $mail_plugins

and add to it (separated by a space):

imap_zlib

After all, there’s no reason not to use compression here, and bandwidth (especially on mobile) is still precious.

Now, edit /etc/dovecot/conf.d/10-ssl.conf, and add 2:

ssl = required
ssl_cert = </etc/dovecot/myserver-full.crt
ssl_key = </etc/dovecot/myserver.key
ssl_cipher_list = ECDHE-RSA-CHACHA20-POLY1305:ALL:!LOW:!SSLv2:!EXP:!aNULL
ssl_protocols = !SSLv2 !SSLv3
ssl_prefer_server_ciphers = yes

(The ssl_cipher_list line, besides setting secure defaults, sets the ChaCha20 protocol as the first one to be tried, since it’s considered one of the fastest and most secure. Note that it’ll require Dovecot linked to LibreSSL or OpenSSL 1.1.x to use that cipher, though Dovecot won’t complain if it doesn’t have access to it; it’ll just use the normal, secure defaults.)

If you don’t change anything else from the default configuration, the server will be listening on port 143 (IMAP) and port 993 (IMAPS). “But,” you ask, “isn’t standard IMAP unencrypted? I thought you were enforcing encryption…” Yes, but the standard IMAP port can be “upgraded” to TLS by entering the STARTTLS command, and email clients not only support that, but typically default to it (if not, just make sure you enable it). The server will refuse to authenticate any users on port 143 before they’ve “STARTTLSed”.

If you wanted to use only IMAP+STARTTLS, or only IMAPS, just edit /etc/dovecot/conf.d/10-master.conf,  look for the “service” configuration, and disable the one you don’t want. But I see no problem with keeping both enabled.

Thoughts? Questions?

(To do the same to SMTP / Postfix, please see How to configure TLS encryption in Postfix.)