A while ago, I wrote (but forgot to document) Yet Another ACME client in Python. You can see the code for this client here.
ACME is a protocol invented by the Let's Encrypt project for automatically requesting TLS certificates from a certificate authority. Other website describe both the protocol and the Let's Encrypt project in much more detail, but in essence they are an attempt to standardize the ad-hoc natural language instructions that most current certificate authorities use.
Let's Encrypt has an unusual feature in that its certificates have a short 90-day validity period and are designed to be automatically issued and renewed. To help with this automation, a huge number of ACME clients have been written by various people. However, various small issues made me unhappy with all of the clients I looked at, so I programmed my own.
This client is designed to only work with the way my particular web server is set up. The reason I felt I needed yet another client was because existing clients all seemed to be a little awkward to use with my particular setup involving many many virtual hosts that do not actually serve files but only redirects (and thus don't have a "web root" in the normal sense).
On my server, this script is run from a per-user crontab belonging to a special
user acme-cert
dedicated for running this script. The crontab is set up to
invoke the following small shell script once a month (giving two whole months
to notice that the script isn't working):
#!/bin/bash
set -eo pipefail
. /storage/certrenewal/venv/bin/activate
set -u
python /storage/certrenewal/le-rqou.py
sudo /bin/systemctl reload nginx.service
sudo /usr/sbin/postfix reload
This script loads a Python virtualenv containing the dependencies for this
script, runs the script itself, and then reloads servers that use the
certificate. Currently, this is only nginx and Postfix. This is allowed because
the sudoers
file contains the following lines:
acme-cert ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx.service
acme-cert ALL=(ALL) NOPASSWD: /usr/sbin/postfix reload
If you examine the source code for this script, you will notice that it has the following constants at the top:
ACCOUNT_KEY_PATH
- Account key JSON file. This file is created by the official Let's Encrypt client certbot. Only RSA keys are supported in this client. This client does not have the ability to generate this file.CSR_PATH
- CSR file. This file should be in PEM format and should contain a list of domains to issue for in the CN/subjectAltName fields.REGISTRATION_EMAIL
- The email address to associate with the account.ACME_CHALLENGE_DIR
- Common directory used for writing challenge files. This client only supports the http-01 challenge, and it requires all domains to server the.well-known/acme-challenge
path from this directory. More on this later.CERT_PATH_TMPL
- A template to create a filename to save newly-issued certificates. The argument{}
will be filled in with a date stamp. Old certificates are not automatically deleted in order to allow for easy rollback. There is currently not a mechanism to automatically prune old certificate files.CERT_PATH_SYMLINK
- A path to a symlink that will be updated to point to the latest issued certificate.CHAIN_PATH
- A path to a file where the certificate chain (issuers) will be saved.
As mentioned above, my server is set up to expect all virtual hosts to serve the
.well-known/acme-challenge
path from the same directory. This is achieved by a
fragment similar to the following in the nginx configuration:
server {
listen 80;
listen [::]:80;
server_name robertou.com www.robertou.com
robertou.net www.robertou.net
robertou.org www.robertou.org
robertou.mobi www.robertou.mobi
robertou.me www.robertou.me
robertou.info www.robertou.info
robertou.us www.robertou.us;
location = / {
return 301 https://robertou.com;
}
location / {
return 404;
}
location /.well-known/acme-challenge/ {
alias /var/www/acme-challenge/;
}
}
This fragment redirects the HTTP root of all domain variants to the HTTPS root
of the "canonical" variant, refuses all paths other than the root (because
there should never be a case where these paths get used as the site is
HTTPS-only), and serves the .well-known/acme-challenge
path from a fixed
location in the filesystem for all domains.