Let’s Encrypt is a new Certificate Authority that provides free SSL certificates. It is intended to be automated, so that certificates are renewed automatically. We’re using Let’s Encrypt certificates for our set of free Calculus practice problems. Our front end is currently served by an Ubuntu server running nginx, and here’s how we have it scripted on that machine. In a future post, I’ll describe how it’s automated on our Docker setup with HAProxy.
First of all, we’re using acme-tiny instead of the official Let’s Encrypt client, since it’s much smaller and, IMHO, easier to use. It takes a bit more to set up, but works well once it’s set up.
We installed acme-tiny in /opt/acme-tiny, and created a new letsencrypt user. The letsencrypt user is only used to run the acme-tiny client with reduced priviledge. In theory, you could run the entire renewal process with a reduced priviledge user, but the rest of the process is just basic shell commands, and my paranoia level is not that high.
We created an /opt/acme-tiny/challenge directory, owned by the letsencrypt user, and we created /etc/acme-tiny with the following contents:
- account.key: the account key created in step 1 from the acme-tiny README. This file should be readable only by the letsencrypt user.
- certs: a directory containing a subdirectory for each certificate that we want. Each subdirectory should have a domain.csr file, which is the certificate signing request created in step 2 from the acme-tiny README. The certs directory should be publicly readable, and the subdirectories should be writable by the user that the cron job will run as (which does not have to be the letsencrypt user).
- private: a directory containing a subdirectory for each certificate that we want, like we had with the certs directory. Each subdirectory has a file named privkey.key, which will be the private key associated with the certificate. To coincide with the common setup on Debian systems, the private directory should be readable only by the ssl-cert group.
Instead of creating the CSR files as described in the acme-tiny README, I created a script called gen_csr.sh:
#!/bin/bash openssl req -new -sha256 -key /etc/acme-tiny/private/"$1"/privkey.pem -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:") <(cat /etc/acme-tiny/certs/"$1"/domains | sed "s/\\s*,\\s*/,DNS:/g")) > /etc/acme-tiny/certs/"$1"/domain.csr
The script is invoked as gen_scr.sh <name>. It reads a file named /etc/acme-tiny/certs/<name>/domains, which is a text file containing a comma-separated list of domains, and it writes the /etc/acme-tiny/certs/<name>/domain.csr file.
Now we need to configure nginx to serve the challenge files. We created a /etc/nginx/snippets/acme-tiny.conf file with the following contents:
location /.well-known/acme-challenge/ { auth_basic off; alias /opt/acme-tiny/challenge/; }
(The “auth_basic off;” line is needed because some of our virtual hosts on that server use basic HTTP authentication.) We then modify the sites in /etc/nginx/sites-enabled that we want to use Let’s Encrypt certificates to include the line “include snippets/acme-tiny.conf;“.
After this is set up, we created a /usr/local/sbin/letsencrypt-renew script that will be used to request a new certificate:
#!/bin/sh set +e # only renew if certificate will expire within 20 days (=1728000 seconds) openssl x509 -checkend 1728000 -in /etc/acme-tiny/certs/"$1"/cert.pem && exit 255 set -e DATE=`date +%FT%R` su letsencrypt -s /bin/sh -c "python /opt/acme-tiny/acme_tiny.py --account-key /etc/acme-tiny/account.key --csr /etc/acme-tiny/certs/\"$1\"/domain.csr --acme-dir /opt/acme-tiny/challenge/" > /etc/acme-tiny/certs/"$1"/cert-"$DATE".pem ln -sf cert-"$DATE".pem /etc/acme-tiny/certs/"$1"/cert.pem wget https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem -O /etc/acme-tiny/lets-encrypt-x1-cross-signed.pem cat /etc/acme-tiny/certs/"$1"/cert-"$DATE".pem /etc/acme-tiny/lets-encrypt-x1-cross-signed.pem > /etc/acme-tiny/certs/"$1"/fullchain-"$DATE".pem ln -sf fullchain-"$DATE".pem /etc/acme-tiny/certs/"$1"/fullchain.pem
The script will only request a new certificate if the current certificate will expire within 20 days. The certificates are stored in /etc/acme-tiny/certs/<name>/cert-<date>.pem (symlinked to /etc/acme-tiny/certs/<name>/cert.pem). The full chain (including the intermediate CA certificate) is stored in /etc/acme-tiny/certs/<name>/fullchain-<date>.pem (symlinked to /etc/acme-tiny/certs/<name>/fullchain.pem).
As-is, the script must be run as root, since it does a su to the letsencrypt user. It should be trivial to modify it to use sudo instead, so that it can be run by any user that has the appropriate permissions on /etc/acme-tiny.
the letsencrypt-renew script is run by another script that will restart the necessary servers if needed. For us, the script looks like this:
#!/bin/sh letsencrypt-renew sbscalculus.com RV=$? set -e if [ $RV -eq 255 ] ; then # renewal not needed exit 0 elif [ $RV -eq 0 ] ; then # restart servers service nginx reload; else exit $RV; fi
This is then called by a cron script of the form chronic /usr/local/sbin/letsencrypt-renew-and-restart. Chronic is a script from the moreutils package that runs a command and only passes through its output if it fails. Since the renewal script checks whether the certificate will expire, we run the cron task daily.
Of course, once you have the certificate, you want to tell nginx to use it. We have another file in /etc/nginx/snippets that, aside from setting various SSL parameters, includes
ssl_certificate /etc/acme-tiny/certs/sbscalculus.com/fullchain.pem; ssl_certificate_key /etc/acme-tiny/private/sbscalculus.com/privkey.pem;
This is the setup we use for one of our server. I tried to make it fairly general, and it should be fairly easy to modify for other setups.
This article was originally published at Hubert’s personal website here.
I’ve modified my original setup to use something similar to this. Note that Let’s Encrypt is now issuing certificates from their X3 CA for better backward compatibility with WinXP (ugh), so any references to ‘lets-encrypt-x1’ should become ‘lets-encrypt-x3’. If not updated, the chain breaks in the latest Firefox version but looks OK in Chrome.