Recently I found myself needing to move an existing WordPress Multisite installation off of a popular shared host. The main goal was to improve the site’s performance (load speed, etc.) and we have more ability to fine-tune things in an environment we fully control.
But it’s been awhile since I tinkered with Multisite and so I didn’t have a current set of “best practices” around how to set configure the nginx server
block to handle subdomains that might be set up on the fly any time the site’s owner wants to add a new “site” to the network.
More importantly, we’ve switched to Let’s Encrypt as our provider for TLS certificates, and when we initially did so, they weren’t yet handling wildcard certificates. They added this capability some time ago now, but this was my first excuse to try it out.
So the goal was: configure nginx and Let’s Encrypt to properly handle any new subdomains added to the WordPess install without having to manually change the server configuration.
Quick Overview of the Tech Involved
We moved the site to a VPS on a Digital Ocean droplet with a LEMP stack with:
- Ubuntu 20.04
- nginx 1.18.0
- MySQL 8.0.22
- PHP 7.4
The other sites hosted on this droplet are mostly standard WordPress sites where the www
subdomain is redirected to the domain name, so they use a more or less standard nginx server
block that we’ve fine-tuned over time.
We’re obtaining TLS certificates from Let’s Encrypt using certbot
and the --nginx
flag to manage the certificate installation process.
First: nginx Config for WordPress Multisite
Our “standard” nginx server
block works really well for WordPress and we get great performance out of a socket connection to php-fpm
.
Our nginx
rewrite rules, however, don’t anticipate the need to handle multiple subdomains that are subject to change over time.
Thankfully, there’s an nginx recipe for WordPress multisite that we were able to use as a starting point. The important thing to remember is that WordPress can be configured to add new sites as subdirectories (e.g. example.com/mysite) or as subdomains (e.g. mysite.example.com). We’re using the subdomain method, and so this recipe was the one we needed.
The recipe calls for a new section before the server
block in the file located at /etc/nginx/sites-available/domain.com
that leverages the nginx map module to set up a variable to handle the various subdomains.
map $http_host $blogid {
default -999;
}
Then inside the server
block itself (i.e. between the server {
and }
for the domain in question), we add some lines that call those variables:
#WPMU Files
location ~ ^/files/(.*)$ {
try_files /wp-content/blogs.dir/$blogid/$uri /wp-includes/ms-files.php?file=$1 ;
access_log off; log_not_found off; expires max;
}
and
#WPMU x-sendfile to avoid php readfile()
location ^~ /blogs.dir {
internal;
alias /var/www/example.com/html/wp-content/blogs.dir;
access_log off; log_not_found off; expires max;
}
Aside from those additions, we’re using our standard set of parameters for a typical WordPress installation.
Next: Get a Wildcard Certificate from Let’s Encrypt
Our typical method is to call certbot
with the --nginx
flag and let it put its ACME protocol token in the /.well-known
subfolder to handle domain validation. This method, known as the HTTP-01 challenge, works well when we’re requesting issuance of a cert for the domain itself and for the www
subdomain.
That request typically looks something like this:
sudo certbot --nginx -d example.com -d www.example.com
But we cannot use the HTTP-01 challenge to request a wildcard cert.
What is a wildcard TLS certificate?
Requests like the one shown above result in the issuance of that is valid for exactly 2 domains: example.com and its subdomain www.example.com.
Thus, when a visitor’s web browser connects to the server and requests a URL containing one or the other of those addresses, the server can legitimately negotiate a TLS connection and encrypt the traffic for it.
But since we’re setting up WordPress as a multisite installation in order to allow the site owner to create new sites on the fly, we aren’t able to predict all of the subdomains that need to be listed on the TLS certificate.
What we need instead is a TLS certificate that is valid for the domain itself (i.e. example.com) and for any subdomain of the domain. Thus, we want to request a certificate using a wildcard to represent the subdomain. In this case, the asterisk character serves as the wildcard, and so we want the cert to be valid for: example.com and all possible subdomains *.example.com.
The problem is that Let’s Encrypt does not permit the issuance of wildcard certificates using the HTTP-01 challenge. Instead, we need to make use of the DNS-01 Challenge.
Configuring the Let’s Encrypt DNS-01 Challenge on the Digital Ocean Platform
The DNS-01 Challenge requires that you prove that you have control over DNS for the domain rather than just a web server for the domain. It works by setting a TXT record for the domain at _acme-challenge.example.com
which contains the ACME protocol token as its value.
As you might imagine, having to create this record manually and then update it every 90 days when Let’s Encrypt needs to renew the certificate would be a painful manual process.
Thankfully, there are DNS plugins for certbot which help automate the process as long as DNS is hosted by one of the compatible providers. Currently, that list includes: Cloudflare, CloudXNS, Digital Ocean, DNSimple, DNS Made Easy, Google, Linode, LuaDNS, NS1, OVH, Route53 (from Amazon Web Services), and any DNS provider that supports dynamic updates as described in RFC 2136.
It was a happy accident that I had decided to use Digital Ocean to host the DNS for this domain. I did it without realizing that I needed this kind of compatibility. So I was pleased to discover that Digital Ocean supports DNS updates via its API and that there’s a certbot plugin for their platform: dns_digitalocean
.
I found some of the documentation around getting this plugin installed on my server a little confusing. One recommendation involved using pip3 (the Python 3.x package manager) to install it. But since I had installed certbot from the Ubuntu standard PPAs using the apt package manager, the version of the plugin that I got using pip3 wasn’t actually connected to the certbot installation I was using.
Ultimately, I realized I could install the plugin I needed using apt like this:
sudo apt install python3-certbot-dns-digitalocean
To fully configure it, I got a shiny new personal access token for the Digital Ocean API from the Applications & API page of my Digital Ocean account.
Then, I created a new file at /home/myloginusername/.secrets/certbot/digitalocean.ini
that looked like this example from the plugin documentation:
# DigitalOcean API credentials used by Certbot
dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
Note: in case this isn’t abundantly obvious, the token shown above is fake and will need to be replaced by a real token that is unique to you and should be treated as if it’s the password to your Digital Ocean account… because anyone with your API token has access to all of the capabilities of their API.
Also, one potential point of confusion I ran across. Since I use sudo
to run certbot
with elevated privileges, I thought perhaps this file should be located in the root
user’s home folder (e.g. home/root/.secrets/...
), but this turned out to be incorrect. It belongs in the home folder for the user that you authenticate with when you log in to Ubuntu.
Also, chmod
that file to 0600
to help keep it safe:
chmod 0600 /home/myloginusername/.secrets/certbot/digitalocean.ini
You shouldn’t need sudo
for that command since it’s in your home folder.
With the certbot dns plugin for your dns provider successfully installed and configured properly, you’re ready to request the cert.
In my case, I wanted to use the dns-digitalocean
plugin to handle the authentication part of the certificate issuance, but I still wanted to use the nginx
plugin to handle the installation of the certificate. This would greatly simplify ongoing maintenance tasks because I’d used the nginx
plugin to handle installation of the other certs on this server.
Thankfully, it’s possible to combine certbot plugins to do exactly this by using the --installer
flag with “nginx” as its value.
The command I used ended up looking something like this:
sudo certbot \
--dns-digitalocean \
--dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \
--dns-digitalocean-propagation-seconds 60 \
--installer nginx \
-d example.com \
-d *.example.com
Bascially, the command tells certbot to create an ACME protocol token, create (or update) the TXT record for this domain using the Digital Ocean API so that the record’s value matches the ACME token, then wait 60 seconds to give DNS a little time to propagate, and then run the DNS-01 challenge and issue/install the cert.
Your Mileage May Vary
Obviously, different server configurations and hosting environments will work differently, but if you happen to be running a VPS with a LEMP stack based on Ubuntu 20.04 and need WordPress Multisite to work with wildcard subdomains and a wildcard TLS certificate from Let’s Encrypt, then this process will generally be workable.
What questions do you have? I hope you found this useful. It’s always great to hear about it either here (feel free to comment below), or you can hit me up on Twitter: @TheDavidJohnson.
Cheers!
Image credit: Fikret tozak on Unsplash