Enabling a Certificate Transparency TLS Extension in Nginx

Certificate transparency is the promise that, eventually, all certificate authorities commonly in root trust stores will publish logs of all certificates they ever issue. These logs would use crypto (Merkle Trees) that, through the watchful eyes of monitors and auditors, makes it nigh impossible for a rogue (or compromised) CA to mis-issue a certificate without showing public evidence thereof.

Like other trust-after-first-use long-lived headers like HSTS and HPKP, there is a new Expect-CT header that can tell clients to reject certificates that don’t show proof of being logged. If you use this header, you know that clients that support it won’t trust a certificate that was mis-issued in a way that evaded the logs. (If you’re gun-shy about potentially breaking your site, the header can be used without the enforce directive which would tell clients to trust a non-CT-qualified cert and tell you about it.)

The “proof” of logging comes in the form of signed certificate timestamps (SCTs). This is a receipt from a public CT log that it received the certificate for inclusion, and that it will add it to the log within a certain time period (usually less than 24 hours). Clients can then verify the legitimacy of the SCT with the CT log directly, or a network of CT monitors and auditors.

These SCTs can be attached to the certificate itself (X.509v3 extension), be provided through OCSP Stapling, or be provided from the web server directly through a TLS extension. The first two require help from your certificate authority, which, at the time of writing, Let’s Encrypt does not do (although it is on their roadmap for 1Q18). So TLS Extension it is!

The capability is not built into Nginx or Apache directly. I will show how to build Nginx from source using a CT Nginx module and a bleeding-edge version of OpenSSL on Ubuntu 17.04, and then we’ll generate the SCT files and configure Nginx. Keep in mind that things move fast on the bleeding edge in TLS land, and these directions may be obsolete within months of posting (particularly the OpenSSL branch we’re selecting).

The zeroth step is to have a working Nginx install with a working TLS configuration! We’re going to reuse the configuration files and even the nginx user for running the process.

The first step is to clone a lot of Github repos; my suggestion is to run all of these commands from /opt:

git clone https://github.com/nginx/nginx.git
git clone https://github.com/openssl/openssl.git -b tls1.3-draft-18 openssl-1.1.1-tls1.3-draft-18
git clone https://github.com/grahamedgecombe/nginx-ct.git
git clone https://github.com/grahamedgecombe/ct-submit.git

This clones the repositories for Nginx, OpenSSL (with a certain TLS 1.3 branch specified, just for fun), a popular Nginx CT module, and software for creating SCT files from CT log servers.

To compile Nginx, I first had to install some extra software on Ubuntu:

apt install build-essential libpcre3 libpcre3-dev zlib1g-dev

Then, from within /opt/nginx, I ran the following long command to configure the compilation. This was put together based on what features I want to add and the flags that the package manager’s version of Nginx uses:

auto/configure --sbin-path=sbin/nginx-custom --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.13.4/debian/debuild-base/nginx-1.13.4=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' --add-dynamic-module=/opt/nginx-ct/ --with-openssl=/opt/openssl-1.1.1-tls1.3-draft-18/ --with-openssl-opt=enable-tls1_3

After this, run make which may take 20 or more minutes. If you get an error, like I did, follow this instruction to edit -lpthread in the Makefile.

At this point, you should disable the package manager’s version of Nginx.

service nginx stop
systemctl disable nginx

Finally, run make install.

You can now copy the nginx.conf and other .conf files from the package manager’s version (such as the entire /etc/nginx/conf.d folder) into your new version of Nginx, which has configuration files live in /usr/local/nginx/conf. Make sure you use the correct PID file:

pid /usr/local/nginx/logs/nginx.pid;

and add the following to load the new CT modules, towards the top of the nginx.conf file:

load_module modules/ngx_ssl_ct_module.so;
load_module modules/ngx_http_ssl_ct_module.so;

In your actual server block, add the following to tell it to send the SCT files and where those files live:

ssl_ct on;
ssl_ct_static_scts /etc/ssl/sct;

Next, create the file /etc/systemd/system/nginx-custom.service so systemd can launch it:

Description=A high performance web server and a reverse proxy server
ExecStartPre=/usr/local/nginx/sbin/nginx-custom -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/local/nginx/sbin/nginx-custom -g 'daemon on; master_process on;'
ExecReload=/usr/local/nginx/sbin/nginx-custom -g 'daemon on; master_process on;' -s reload
ExecStop=/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /usr/local/nginx/logs/nginx.pid

It’s now time to generate the SCT files. First, from within /opt/ct-submit, you’ll have to make the executable: go build (If you need to, apt install golang-go). You’ll then need to pick at least three logs from this page; I picked a couple Google servers, Comodo, and Digicert. You specify the log, pass the full chain in through stdin, and create the SCT from stdout. Note this process has to be repeated for every certificate renewal, which will require automation if you’re mating it with an automated renewal process like a cronjob for Let’s Encrypt.

./ct-submit ct.googleapis.com/pilot < /path/to/fullchain.pem > /etc/ssl/sct/google-pilot.sct
./ct-submit ct2.digicert-ct.com/log < /path/to/fullchain.pem > /etc/ssl/sct/digicert2.sct
./ct-submit sabre.ct.comodo.com < /path/to/fullchain.pem > /etc/ssl/sct/comodo-sabre.sct
./ct-submit ct.googleapis.com/rocketeer < /path/to/fullchain.pem > /etc/ssl/sct/google-rocketeer.sct

And, finally, test your config and start your new Nginx service:

/usr/local/nginx/sbin/nginx-custom -t
systemctl daemon-reload
systemctl start nginx-custom.service

As always, use the Qualys SSL Server Test to test your config, looking for “Certificate Transparency” to say “Yes (TLS Extension).” You can also look in the Security tab of Chrome/Chromium’s developer tools.

With this setup, you also might be able to do some fun things like:

  • Enabling CHACHA20/POLY1305 cipher suites
  • Enabling the X25519 curve for ECDH
  • Enabling TLS 1.3 (Draft 18)

If you ever want to go back to your old version of Nginx:

systemctl stop nginx-custom.service
service nginx start

When you’re ready to commit to CT for good, consider enabling the Expect-CT header, using the excellent instructions from Scott Helme.