Supercharging , Part 2

As fast as the Internet moves, even 's latest release seems dated. Let's fix that again.

Published

Introduction

Last year, I wrote about how I took NGINX's and OpenSSL's sources and applied some Cloudflare patches to it. Since then, NGINX has kept progressing, OpenSSL formally released TLS 1.3 to the masses, and that blog post has gotten a little out of date. So, here's how I'm revising it for the tail end of 2018.

In the past year, HTTPS has shown no signs of slowing down, with Let's Encrypt allowing for wildcard certificates and TLS 1.3 becoming an approved Internet standard. HTTP/2 is now commonplace, and now the Internet Engineering Task Force is talking about creating HTTP/3 out of another Google product, QUIC. All of the major CA's have started embedding SCT's into their certificates, all but cementing the idea of Certificate Transparency.

In terms of last year's post, NGINX now has a mature HTTP/2 implementation with HPACK support, so that's one old patch we can forget about. OpenSSL is now two releases into its 1.1.1 branch, where ChaCha20 and Poly1305 are there to stay, so we can forget that patch, too. However, there will still be room for improvement on both fronts. Finally, Cloudflare's patch for dynamic TLS record resizing had to be removed because NGINX wouldn't compile with that and the other patches mixed together.

Getting Started

Find some space on some fast storage, like /tmp, and make a folder we can work in.

# My tmpfs is a little small, so I'm going to toss this under /opt.
mkdir -p /opt/nginx-build-part-2
cd /opt/nginx-build-part-2

OpenSSL

Before we even delve into NGINX, let's get a copy of OpenSSL ready to build. Lucky for us, the NGINX build process can automatically build a copy of OpenSSL. Since the last time we played with NGINX, OpenSSL released their 1.1.1 branch, not that Ubuntu 18.04 would know, as it's stuck on the 1.0.2 branch. When developing 1.1.1, the OpenSSL community removed weaker algorithms and protocols like MD5, SSL, and 3DES; and they also added on newer ones like Poly1305, TLS 1.3, and ChaCha20 -- all of which we need, in my opinion.

Download and verify

Again, if OpenSSL 1.1.1a is not the most recent release by the time you read this, grab the newest one instead. Of all the potentially buggy software to run, your crypto library is the worst place to have a problem!

# Download and verify OpenSSL.
wget https://www.openssl.org/​source/​openssl-1.1.1a.tar.gz
wget https://www.openssl.org/​source/​openssl-1.1.1a.tar.gz.asc
gpg --recv-keys 0xD9C4D26D0E604491
gpg --verify openssl-1.1.1a.tar.gz

With Respect to Bill Gates, 128 Bits Ought to be Enough for Anyone

TLS 1.3 is so different from its predecessor that I argue it should have been called TLS 2.0. One change we need to know is that the cipher suites are fixed unless an application explicitly defines TLS 1.3 cipher suites; changing NGINX's usual list of cipher suites won't cut it! Thus, all of your TLS 1.3 connections will use AES-256-GCM, ChaCha20, then AES-128-GCM, in that order. Because I don't trade in government or medical data, I prefer to use 128-bit encryption, and also add the two missing CCM-mode suites in case anything decides to support them in the future. To do this, we need to edit the source ourselves -- I couldn't find a patch.

Fire up your favorite text editor, and open up openssl-1.1.1a/include/openssl/ssl.h. Look for these lines (starting at line number 175 in OpenSSL 1.1.1a):

/* Before */
# if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305)
#  define TLS_DEFAULT_CIPHERSUITES "TLS_AES_256_GCM_SHA384:" \
                                   "TLS_CHACHA20_POLY1305_SHA256:" \
                                   "TLS_AES_128_GCM_SHA256"
# else
#  define TLS_DEFAULT_CIPHERSUITES "TLS_AES_256_GCM_SHA384:" \
                                   "TLS_AES_128_GCM_SHA256"<
#endif
					
				

Once you've found them, modify the two #define's to look like this, and be careful with the colons, quotes, and end-of-line escaping:

/* After */
# if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305) # define TLS_DEFAULT_CIPHERSUITES "TLS_AES_128_GCM_SHA256:" \ "TLS_AES_128_CCM_SHA256:" \ "TLS_AES_128_CCM_8_SHA256:" \ "TLS_CHACHA20_POLY1305_SHA256:" \ "TLS_AES_256_GCM_SHA384" # else
/* We're definitely building with ChaCha20-Poly1305,
so the "else" won't have any effect. Still... */
# define TLS_DEFAULT_CIPHERSUITES "TLS_AES_128_GCM_SHA256:" \ "TLS_AES_256_GCM_SHA384"
#endif

This modifies the preprocessor's variables such that when we build OpenSSL later, TLS 1.3 connections will default to 128-bit encryption for a minor speed boost.

Otherwise, the OpenSSL 1.1.1 is good enough on its own, so let's work on NGINX!

NGINX

Find the Source

As of this writing, the current version is NGINX 1.15.7, which adds some security patches and minor feature updates to that stable branch. Download it or the newest version; then, run these commands to download, verify, and unpack it.

#!/bin/sh
# On the off-chance you don't have wget and gpg, let's get them.
# I'm using Ubuntu -- use your own distro's package manager.

apt install -y wget gpg2

# Download and verify this release of NGINX.
wget https://nginx.org/download/nginx-1.15.7.tar.gz
wget https://nginx.org/download/nginx-1.15.7.tar.gz.asc
gpg --recv-keys 0x520A9993A1C052F8
gpg --verify nginx-1.15.7.tar.gz

# If it verified, let's keep going.
# Unpack and chdir into it.

tar xzf nginx-1.15.7.tar.gz
cd nginx-1.15.7/

The unmodified source is here. Let's start messing with it. If you accept payment card information, protected health information, or run any service where an outage might upset anyone, then I suggest you stop reading, because we are about to void any warranty you might conceivably have and risk your job in the name of speed and an A+ rating on SSL Labs.

Do Androids Dream of ChaCha20?

On modern Intel, AMD, and Apple processors, the common AES-GCM cipher and mode are sped up by dedicated hardware, making that algorithm's implementation faster than anything by a wide margin. On older or cheaper processors that lack that feature, though, the ChaCha20 cipher runs faster than AES-GCM, as was the ChaCha designers' intention.

When a Web browser starts a secure connection, it sends the remote server a list of ciphers that it knows and is willing to use. Some Web browsers such as Firefox, Chrome, and the Android Browser will look at the hardware it's running on, and if it's a chip without hardware-accelerated AES, it will ask the server for ChaCha20 before AES-GCM. However, any well-meaning Web server will tell the client which cipher it's going to use by giving it a list that the administrator has selected from, and the client is required to pick the first one that it knows, even if it's not the fastest or more secure.

Eventually, the browser makers sat down with the server software developers, and they came with a dynamic re-ordering scheme. If a client asked for ChaCha20 over AES-GCM, the Web server would quickly re-order its list of pre-approved ciphers and put ChaCha20 first, so the TLS connection would be a little bit faster.

However, while OpenSSL 1.1.1 supports it, NGINX does not support it. Of course, we have a patch for NGINX.

cd nginx-1.15.7/

# Apply the ChaCha20 reordering patch.
curl https://raw.githubusercontent.com/kn007/patch/master/nginx_auto_using_PRIORITIZE_CHACHA.patchpatch -p1

Bring Back SPDY (for Browsers that Don't Know Better)

A while back, Google devised a replacement for HTTP/1.1, which they called SPDY. It provided many benefits, in part by changing the convenient human-readable HTTP protocol into a smaller, speedier, binary one. By the time SPDY had reached its third major version, SPDY was selected for refactoring into what is now HTTP/2. All of the major browsers added support for HTTP/2, understandably at the same time they removed support for SPDY, which got sent into the Recycle Bin of computing history.

However, there are still plenty of older Android 6.0 devices in use that speak SPDY/3.1 and not HTTP/2. This patch, provided by Cloudflare, adds support for SPDY/3.1 back into NGINX alongside the official HTTP/2 support. Unlike with TLS, there is no security risk in supporting the older protocol (as far as I know), so let's do it.

# Allow HTTP/2 and SPDY/3.1 to coexist peacefully.
curl https://raw.githubusercontent.com/cloudflare/sslconfig/master/patches/nginx__1.13.0_http2_spdy.patchpatch -p1

Finally, if you have hardware stuck on Windows 8, Windows RT, or Windows Phone 8 and you're wondering why SPDY isn't being negotiated, Microsoft only added support for SPDY/3.0, which is a tad too old to be supported by this patch. You did what you could.

Compressing Resources with Brotli

This part is unchanged from last year. There are two modules that I'll be adding in. The first is the Brotli compression module, which is far more efficient than GZip, which will save some bandwidth for you and your users. Another Google invention, Brotli is in most of the major browsers, so there's no reason to not use it.

This will require a couple libraries to be installed; fortunately, Scott Helme's blog proved to be an invaluable resource in getting this set up.

You will be needing the Git client and some Python libraries for the rest of this tutorial, so let's install those, too. Then, we'll fetch the Brotli libraries and module.

cd ..
apt-get install git python2.7 python-dev
git clone https://github.com/google/brotli.git
cd brotli
sudo python setup.py install
cd tests
make
cd ..

git clone https://github.com/bagder/libbrotli
cd libbrotli
./autogen.sh
./configure
make
sudo make install
cd ..

git clone https://github.com/google/ngx_brotli.git

Finally, Let's Build .

Now that we have all those pieces, we can finally get around to building OpenSSL and . I'm going to place it into /usr/local so Ubuntu updates don't potentially clobber our hard work.

./configure --prefix=/usr/local --conf-path=/usr/local/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_gzip_static_module --with-http_sub_module --with-http_xslt_module --with-http_v2_module --with-http_spdy_module --with-openssl=../openssl-1.1.1a --with-openssl-opt='enable-ec_nistp_64_gcc_128 enable-egd' --add-module=../ngx_brotli
make
sudo make install

Congratulations! You now have a more modern, bleeding-edge version of waiting for you under /usr/local/sbin. If you're using systemd, you can run systemctl enable nginx && systemctl start nginx.

Server and Site Configuration

Before we can serve websites, though, we should modify the server-wide configuration inside /usr/local/etc/nginx/nginx.conf. Let's add support for everything we shoehorned into NGINX. First, Brotli compression:

# Compress assets with Brotli on the fly.
# Files with MIME types listed under brotli_types will be compressed,
# to brotli_comp_level (suggested value is 6, pick a value 1-11).
brotli on;

brotli_comp_level 6;
brotli_window 16m;
brotli_types text/* image/svg+xml application/json application/ld+json application/javascript application/xml application/rdf+xml application/rss+xml application/atom+xml application/pgp-keys;

# If you've taken the time to pre-compress assets, then serve those
# instead! For example, if there's a file index.html being sent to a
# browser that understand Brotli, NGINX will serve index.html.br to it.
#
# You can pre-compress assets with: brotli --quality=11 filename
brotli_static on;

TLS Settings

Every website should be HTTPS.

Dual Certificates

Separately from this tutorial, I've used Let's Encrypt's Certbot to generate ECDSA and RSA certificates for this site. NGINX supports dual certificates, so you can get the leaner, meaner ECC certificates but still let visitors with older browsers browse your Web site:

ssl_certificate     /path/to/certs/rsa2048.crt;
ssl_certificate_key /path/to/certs/rsa2048.key;
ssl_certificate     /path/to/certs/ecdsa256.crt;
ssl_certificate_key /path/to/certs/ecdsa256.key;

I Believe You Have My Stapler

OCSP stapling allows revocation information to be served with the certificate, saving your browser from doing the work of revocation checking (or not doing it at all). Though this is optional, I issue my certificates as Must-Staple, so this is mandatory for me.

# Enable OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

# OCSP stapling requires DNS servers. How about Cloudflare?
resolver [2606:4700:4700::1111] [2606:4700:4700::1001] 1.1.1.1 1.0.0.1;

# You need to put the Let's Encrypt Authority X3 certificate in this file.
ssl_trusted_certificate /usr/local/etc/nginx/ssl-stapling.pem;

Strong Ciphers

This is where it gets a little crazy. This little blog doesn't have to be super-secure, so I'm going to allow Early TLS and pretty much any good cipher or curve to be used. If you're running a site that actually needs security, consider disabling TLS 1.0/1.1 and CBC-mode ciphers.

# Allow any version of TLS.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

# As always, let the server pick the cipher.
ssl_prefer_server_ciphers on;
# Other than that, here's how to get the most speed:
# - Offer ECDSA before RSA.
# - Offer elliptic-curve DHE before regular DHE.
# - Offer 128-bit encryption before 256-bit encryption.
# - Offer AEAD ciphers (AES-GCM, AES-CCM, and ChaCha20) before -CBC.
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-CCM:ECDHE-ECDSA-AES128-CCM8:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-AES256-CCM8:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-CAMELLIA128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-CCM:DHE-RSA-AES128-CCM8:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-CCM:DHE-RSA-AES256-CCM8:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-SHA256:DHE-RSA-CAMELLIA128-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-CAMELLIA256-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-RSA-SEED-SHA:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:EDH-DSS-DES-CBC3-SHA";

# Here are Mozilla's recommended DH parameters from RFC 7919:
ssl_dhparam /usr/local/etc/nginx/ffdhe2048.pem;

# For ECDHE and/or ECDSA, we need to pick an elliptic curve.
# Allow any secure cipher (≥256 bits) that the client asks
# for, sorted by strength.
ssl_ecdh_curve "X25519:brainpoolP256r1:prime256v1:secp256k1:sect283k1:sect283r1:brainpoolP384r1:secp384r1:sect409k1:sect409r1:brainpoolP512r1:secp521r1:sect571k1:sect571r1";

# Keep sessions alive for a while.
# Lower this if your site is popular or your server is underpowered.
ssl_session_cache shared:SSL:30m;
ssl_session_timeout 15m;

# Some people say to disable session tickets.
# I leave them on because I'm just an unimportant personal site.

ssl_session_tickets on;

Site Definitions

The only changes we need to make to the site definitions are to make sure HTTP/2 and SPDY are both enabled. Find your server block and make this quick change:

server {
    listen *:443 ssl http2 spdy default_server;
    listen [::]:443 ssl http2 spdy default_server;
    server_name rhymeswithmogul.com *.rhymeswithmogul.com;
    root /path/to/www/rhymeswithmogul.com/htdocs;
    index index.html;

    # the rest of your site info goes here
}

If there are any typos or if I can clarify anything better, feel free to complain at me on Twitter.

Metadata and license information
Property Value
Author Me, wearing a purple shirt, tie, and a jacket. Colin Cogle
License

This blog post is licensed under Creative Commons License 4.0 CC-BYCreative Commons Attribution 4.0 International License.

Content Rating None
Encoding text/html; charset=UTF-8
Permalinks
Search keywords , HTTP, , , TLS
Summary
Word count 1,540 words, not counting code blocks

Creative Commons License 4.0 CC-BY This blog post is licensed under a Creative Commons Attribution 4.0 International License. The full text of the license is available online at https://creativecommons.org/licenses/by/4.0

Links

This article:
http://rhymeswithmogul.com/scnginx2
"Brotli Compression" by Scott Helme:
https://scotthelme.co.uk/brotli-compression/