Enable TLS 1.3 in Nginx on Debian Stretch

The PCI Council of Elders have recently forbidden the use of the old TLS 1.0 standard in all public-facing web servers.

It’s time. TLS 1.0 had a good life, but now it is three versions out of date and a liability to all site visitors, not just those using dangerous old technology.

To disable it, open your Nginx configuration and change the ssl_protocols entry to:

ssl_protocols TLSv1.2;

You’ll notice the above also drops TLS 1.1 support. This isn’t (yet) mandatory, however it was one of those standards that just kind of fell through the cracks. Virtually all browsers and networking middleware supporting 1.1 also support 1.2 or newer, so there’s little harm in dropping this also-old protocol.

The Road to Hell TLS 1.3

Like all things security, the effectiveness of a given version of TLS changes over time. As such, the standard was written with adaptability in mind. Unfortunately, the Internet is rather large and complicated, and a depressingly large number of software and hardware didn’t implement its predecessors correctly. As a result, the road from 1.2 to 1.3 proved… challenging.

In the end, it took more than two dozen spec drafts and several years to settle on The New Standard, which was finally made official this March. But it got there! TLS 1.3 comes with a number of improvements, including much more secure ciphers (while disabling support for many weak and dangerous ones), perfect forward secrecy, and fewer roundtrips during handshakes, which leads to better performance.

In the months since the standard was approved, most major web browsers have landed support for the protocol, so millions of users are ready and waiting.

There’s just one problem: adoption on the server-side has been glacial.

Fuck You, OpenSSL

The problem largely comes down to the ubiquitous, bloated OpenSSL library that handles all things SSL for pretty much the entire planet. You’d think that THE world’s premiere SSL library would be first in line to adopt changes to SSL standards, but, well, no. As of this writing, the latest stable branch lacks support for TLS 1.3. And that’s upstream; for Linux distributions like Debian or CentOS, the bundled versions of OpenSSL are even older.

Unfortunately, protocol support is an All or Nothing proposition. Even if client’s web browser and the server software (e.g. Nginx) support TLS 1.3, the magic won’t happen if OpenSSL doesn’t.

Thankfully, OpenSSL isn’t the only game in town.

A Boring Workaround

After a series of major and catastrophic bugs in the OpenSSL library imperiled the entire planet, people started to wonder whether it was a good idea to place so many eggs in a single basket. As a result, a handful of alternative SSL libraries began springing up, including BoringSSL and LibreSSL.

As of this writing, BoringSSL contains support for TLS 1.3, and Nginx 1.13+ also supports TLS 1.3. If the two are built together, then your server, too, can support TLS 1.3!

Before we begin, it is worth noting a few things:

  • BoringSSL is maintained by Google, for Google. If you are uncomfortable with Google, you should wait until TLS 1.3 support lands in OpenSSL or LibreSSL.
  • BoringSSL does not support OCSP Stapling. I think the benefits of TLS 1.3 outweigh the benefits of OCSP stapling, but if you disagree, again, it is better to wait for another library to land support.

Making It Work On Stretch

By default, Debian Stretch provides Nginx 1.10. The backports channel, however, provides Nginx 1.13, which is what we’ll need.

On that note, make sure the backports repos are in your /etc/apt/sources.list:

deb http://ftp.us.debian.org/debian stretch-backports main
deb-src http://ftp.us.debian.org/debian stretch-backports main

If they weren’t, add them and run sudo apt-get update to refresh.

If you are currently running the 1.10 branch of Nginx, you’ll need to upgrade to backports. Depending on your system, this might be as simple as running sudo apt-get install -t stretch-backports nginx, but sometimes backports take such a back seat that APT won’t automatically resolve dependencies. If you get a big list of errors, you might need to adapt that line to include every currently-installed Nginx package, e.g. … nginx-common libnginx-mod-http-echo …. Once you’ve officially switched over, though, updates should come through without any undo effort.

To ensure compatibility with all the different Nginx packages provided by Debian, we’ll be building from the Debian source rather than the raw upstream source. This way you’ll get a drop-in replacement deb package and not have to go through and retool everything.

Debian provides three flavors of Nginx — light, full, and extras — but they are all built the same way. For this tutorial, we’ll be using “nginx-full”. If you’re running a different flavor, just substitute any mention of “full” with “light” or “extras” respectively.

To get started, make let’s fetch some build dependencies:

# Get what it takes to build the Debian source:
sudo apt-get build-dep -t stretch-backports nginx-full

# We'll also need Git and Cmake.
sudo apt-get install git cmake

One last build dependency: Ninja. Download the latest release, extract it, and add the ninja executable to your $PATH (or move it to somewhere like /usr/local/bin).

Depending on your system, you might need some additional packages, but you’ll be notified during the build if anything is missing.

All right, onto the build!

# Make a temporary directory and go to it.
mkdir /tmp/nginx && cd /tmp/nginx

# Get the Debian sources.
sudo apt-get source -t stretch-backports nginx-full

# You should now have a folder called "nginx-1.13.3"; if it
# named something else, just mentally substitute that path
# throughout the rest of the guide.

# Clone BoringSSL.
git clone https://boringssl.googlesource.com/boringssl /tmp/nginx/nginx-1.13.3/boringssl && cd /tmp/nginx/nginx-1.13.3/boringssl

# While BoringSSL supports TLS 1.3, at the time of this writing,
# it does not support Nginx's TLS 1.3 flag. There's a simple
# patch to fix this, but depending on when you read this, this
# step might not be necessary.
wget https://gitlab.com/buik/boringssl/raw/boringssl-patch/Enable-TLS13-BoringSSL-23-06-18.patch && git apply Enable-TLS13-BoringSSL-23-06-18.patch && rm Enable-TLS13-BoringSSL-23-06-18.patch

# We now need to pre-build BoringSSL.
mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -GNinja ../ && ninja && cd ..

# Nginx's build script follows OpenSSL's structure, however
# BoringSSL is laid out a little differently. We can fix most
# issues by making some symlinks.
mkdir -p .openssl/lib && cd .openssl && ln -s ../include . && cd ../ && cp build/crypto/libcrypto.a build/ssl/libssl.a .openssl/lib && cd ..

There is one more OpenSSL/BoringSSL compatibility issue that needs to be resolved. Open /tmp/nginx/nginx-1.13.3/auto/lib/openssl/conf in your editor of choice, and change:

CORE_INCS="$CORE_INCS $OPENSSL/.openssl/include"
CORE_DEPS="$CORE_DEPS $OPENSSL/.openssl/include/openssl/ssl.h"

to:

CORE_INCS="$CORE_INCS $OPENSSL/include"
CORE_DEPS="$CORE_DEPS $OPENSSL/include/openssl/ssl.h"

(In other words, remove the “.openssl/” bit.)

Once you’ve done that, we need to add the following two flags to the “common” section of /tmp/nginx/nginx-1.13.3/debian/rules:

# Add these after --with-ld-opt:
--with-openssl=/tmp/nginx/nginx-1.13.3/boringssl \
--with-openssl-opt=enable-tls1_3 \

That’s it! Now you’re ready to build:

# Get back to the source root:
cd /tmp/nginx/nginx-1.13.3

# Make the deb packages.
dpkg-buildpackage -b

Once that is done running, you should have a few million *.deb packages in the parent /tmp/nginx directory. You can ignore all of them except for the one corresponding to your flavor (light, full, or extras). That one package can be installed the usual way: sudo dpkg -i /tmp/nginx/THE-DEB-FILE.

Just in case the upgrade didn’t restart the Nginx process, run: sudo service nginx restart.

Update Your Nginx Config

The following global cipher/protocol-related settings can go in the http{} block of your /etc/nginx/nginx.conf file:

ssl_ciphers '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE+AES128:ECDHE+AES256:RSA+AES256';
ssl_ecdh_curve secp384r1:X25519:P-256:P-384;
ssl_prefer_server_ciphers On;
ssl_protocols TLSv1.2 TLSv1.3;

The bracket syntax in the cipher list is unique to BoringSSL and helps ensure that those fancy options are only used when they can be used. If you built Nginx against a different SSL library, you might have to format that line differently.

It is worth noting the above allows for two different types of comparatively weak connections, a necessary evil to avoid “Proxy Error” messages from clients running versions of Windows < 10. If and when older versions of Windows are dead and buried, you can replace the curve list with ssl_ecdh_curve secp384r1:X25519.

You can test the config with nginx -t. If you had been using OCSP stapling, you can temporarily comment out any ssl_stapling on rules through your configs to remove the warnings.

If all is good, re-restart Nginx (sudo service nginx restart) and you should now be TLS 1.3-capable! To confirm your server is actually using it, you can either visit your site in Chrome or Firefox and check out the certificate info, or visit Qualys SSL and run a scan against your site.

Maintaining It

Because you have built and installed a custom package, you’ll need to be mindful of future updates to the original. Debian updates are few and far between, but if the backports packages get a version bump, just repeat the above build steps to compile a new version.