Variable SSL certificate directives in nginx (part 2)

Feeling encouraged by my friend Jeremy Felt’s blog post on the subject, I thought I may finally be able to achieve the panacea of WordPress Multi-Network SSL configurations:

  • Multisite (subdirectory install type, with subdomains anyways)
  • Multi-Network (using the WP Multi Network plugin)
  • LetsEncrypt Wildcard Certificates for every domain
  • Behind an nginx proxy server, using a single set of SSL directives

In the past few years (really since LetsEncrypt saved the web) a bunch of really smart folks have invested a bunch of time into trying to address that last bullet point.

LetsEncrypt traditionally stores its SSL certificates in the following location:

/etc/letsencrypt/live/example.org/fullchain.pem;

What we need to do, is take example.org and make it a variable.

To this day, if you search the web for anything similar to “nginx SSL variables” you’ll find dozens of (now out of date) articles, where frustrated folks are attempting to do the impossible – use nginx variables inside the ssl_certificate and ssl_certificate_key configuration settings.

Since nginx 1.15.9, this is now possible. It isn’t incredibly straightforward, but it is possible. Here’s what you need:

  1. Latest version of WordPress (5.1 or higher)
  2. Latest version of WP Multi Network (2.2.0 or higher)
  3. Latest version of nginx (1.15.9 or higher)
  4. Latest version of openSSL (1.1.0j or higher)

As I write this, nginx 1.15.9 isn’t the latest package available, so to use this you’ll need to compile it from source. This isn’t for everyone, and there are repercussions for doing it wrong (trust me on this) but once it’s working, it’s really-really sweet, so at the very least you may want to spin up a test server and give it a go.

I searched the web for quite a few hours looking for anyone that got this working, and frankly, no one really has other than Jeremy. I even dug into the nginx Trac and found issue 1744 where Maxim Dounin was kind enough to gently nudge me in the right direction.

He said:

Variables set with the set directive of the rewrite module are only available after rewrite instructions were evaluated when processing a request, see ​rewrite module documentation

When loading certificates you have to use builtin connection-related variables, or custom variables which are always available – such as provided with ​map​geo​perl_set, or ​js_set.

Loosely translated, that means you can’t just use any variable you’ll find in the nginx variables list; and you can’t just set your own the way probably expected to, you need to “map” them from a variable that is guaranteed to always exist.

Here’s what I came up with:

map $ssl_server_name $ssl_domain_name {
volatile;
hostnames;
default 'example.org'; # Your "root" WordPress site
~^((?<subdomain>.*)\.)(?<domain>[^.]+)\.(?<tld>[^.]+)$ $domain.$tld;
}

In the above block, we

  • map from $ssl_server_name (which is an nginx variable that is always available)
  • to $ssl_domain_name (a new nginx variable of our own creation)
  • with a small regular expression that splits $ssl_server_name up into 3 separate variables, but only returns 2 of them: domain and tld
  • The volatile and hostnames directives are optional, and only exist as speed-ups to hint to nginx what the value might be

I’m able to do this because I know that every single website I’m serving will be in that format, with $subdomain being optional.

Then, when it comes time to configure nginx to look for the SSL certificates:

ssl_certificate           /etc/letsencrypt/live/$ssl_domain_name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$ssl_domain_name/privkey.pem;

A sample nginx.conf might look something like this:

user                root; # or www-data if you can figure out perms
error_log /var/log/nginx/error.log crit;
pid /var/run/nginx.pid;
worker_processes auto;

events {
worker_connections 1024;
multi_accept on;
use epoll;
}

http {

# Get your SSL domain.tld
map $ssl_server_name $ssl_domain_name {
volatile;
hostnames;
default 'example.org';
~^((<subdomain>?.*).)(?<domain>[^.]+).(?<tld>[^.]+)$ $domain.$tld;
}

server {
listen 80;
listen [::]:80;
limit_req zone=http burst=5;
server_name _;
server_name_in_redirect off;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

# Cert locations
ssl_certificate /etc/letsencrypt/live/$ssl_domain_name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$ssl_domain_name/privkey.pem;

# Tons of other things will go here
}

# Site specific things
include /etc/nginx/sites-enabled/*;

# Tons of other things (location blocks, etc...) will go here
}

I’m working on publishing by advanced WordPress/LetsEncrypt/nginx configuration to a Git repository, and will link to that here as soon as it’s cleaned up and ready to go.