bram@cbbg.nl~$ hexdump cbbg.bin 00 | CA BB A9 E0 00 42 72 61 6D 27 73 Ê»©à.Bram's 0B | 72 65 73 69 64 65 6E 63 65 00 00 residence.. 16 | 6F 6E 20 74 68 65 20 77 65 62 2E on the web.

Automating TLS certificates on FreeBSD

“Let’s Encrypt certificate expiration notice” a dreaded e-mail subject. After several more reminders I would login to my webserver and run the holy certbot trinity: service nginx stop, certbot renew, service nginx start. And every time I would run these commands I would think, I really should automate this. The final push to actually do this was when I learned that the certificate lifetime would be reduced to 47 days (Ballot SC081v3. So after many years of manual renewal, I decided to document my automated certificate renewal process.

For the automation process I decided to use acme.sh which is written purely in POSIX shell as compared to the official certbot client which needs an entire Python environment. A minor caveat is that acme.sh uses ZeroSSL as its CA instead of Let’s Encrypt. I decided to go with the default, but it should be easy to change.

Preparations

Install acme.sh (duh!) on FreeBSD it has the acme user associated with it and it uses /var/db/acme by default for doing its work. The ACME client requires two locations, a place to put a secret token which will be served using the web server and a location to put the actual certificates. For the token location I will use `/usr/local/www/acme/’ and will set the owner to acme:

mkdir /usr/local/www/acme
chown acme:acme /usr/local/www/acme/

The next location is where the certificates will be installed. I decided to use /usr/local/etc/ssl/acme/ for this directory, the owner will also be set to acme.

Allow acme to reload nginx

During the certificate issuing and renewal process the acme user needs permission to reload the nginx webserver. The BSD style equivalent of sudo seems to be doas, so I will use this, and it is already in the ports collection (hooray 🎉). The following addition to the config file allows the acme user to run service nginx reload without entering a password.

# /usr/local/etc/doas.conf
permit nopass acme as root cmd service args nginx reload

nginx

Another minor preparation step is to let nginx serve the secret token, this can be done by adding the following location block to the nginx config file:

location /.well-known/acme-challenge/ {
    root /usr/local/www/acme;
    try_files $uri =404;
}

Issuing

After these exhausting preparations, it is time to issue some certificates. The first time this is done manually and the parameters are stored in /var/db/acme. The commands are run as the acme user, hence the su acme at the beginning.

su acme -c 'acme.sh --issue -d cbbg.nl -d www.cbbg.nl --webroot /usr/local/www/acme/'
su acme -c 'acme.sh --install-cert -d cbbg.nl --key-file /usr/local/etc/ssl/acme/cbbg.nl_privkey.pem --fullchain-file /usr/local/etc/ssl/acme/cbbg.nl_fullchain.pem --reloadcmd "service nginx reload"'

What about file permissions? Isn’t the nginx web server run as the www user and the TLS directory is owned by the acme user? That’s correct, but remember that the main nginx process is run by root, this main process also loads the TLS certificates, so no problem.

Cron magic

And finally to top it off, a cron entry. Run crontab -u acme -e to edit the cron file for the acme user. I decided to run the certificate renewal command every Wednesday at 02:42 (just seemed a fun time run it).

# Should run every Wednesday at 02:42
42 2 * * wed acme.sh --cron --reloadcmd "doas service nginx reload"

Now, after all these years I keep thinking “was it really that easy?” I should have done it way earlier.

Tags: