Installing acme-tiny
My favorite Let's Encrypt clients is acme-tiny. It is just a very short Python script, which makes a code review easy. Som preparations must be done with OpenSSL. acme-tiny
does not require root privileges. It just works. The user lets
has to exist on an OpenBSD system with an nginx web server for the following description.
Creating the directoy structure
Create the following directories in the home directory of lets
.
mkdir bin
mkdir src
mkdir reqs
mkdir certs
Cloning the repository
There may be updates of the script in the future. To facilitate this we are cloning the Github repository into the src
directory.
cd src
git clone https://github.com/diafygi/acme-tiny.git
cd
Now you should always review the source code of the script. If the code review is showing no issue, we are copying the script into the bin
directory.
cp src/acme-tiny/acme_tiny.py bin
chmod +x bin/acme_tiny.py
Creating a private key for the Let's Encrypt account
You will need a key pair for the account. The following command will generate a private and a public key for your account. The private key should only be readable by the user lets
.
openssl genrsa 4096 > account.key
chmod 400 account.key
Creating a certificate signing request (CSR)
You must create a a domain key pair and certificate signing request for each domain. The certificate signing request contains the public key for the domain. It will be send to Let's Encrypt for signing.
openssl genrsa 4096 > example.key
openssl req -new -sha256 -key example.key -subj "/CN=example.com" > ./requests/example.csr
Only the server process will require the private key from now on. I move it as user root
(sudo/doas) into the /etc/ssl/private
directory, where only root
can access it.
It is also possible to request one certificate for several domains, but I do not use this at the moment.
Configuring the challenges directory
Let's Encrypt must verify the you have control over the requested domain. This is done by checking a special file that the acme-tiny client puts on the web server at http://domain/.well-known/acme-challenge/....
server { listen 80; server_name example.com; location /.well-known/acme-challenge/ { alias /var/www/challenges/; try_files $uri =404; } ... }
This step kept me for quite some time. The reverse proxy did search in the wrong directory, but the configuration seemed to be quite in order. I had to restructure my nginx configuration files. Now it is working like a charm. The lesson learned is: Do not overoptimize configuration files.
Now we have completed the preparations for the certificate requests.
Requesting a certificate
The following command request the certificate for the example domain.
python acme_tiny.py --account-key ./account.key --csr ~/requests/example.csr --acme-dir /var/www/challenges/ > ~/certs/example.crt
Normally a server requires a certificate chain. The certificate chain is a file with the concatenation of the domain and the intermediate and root certificate of the certification authority. The following steps create this file.
curl -fsS https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem -o ~/certs/intermediate.pem
cat ~/certs/example.crt intermediate.pem > /etc/ssl/lets/example-chained.pem
This shows the trick to separate the privileges. The domain's private key (example.key
), which is only required during creation of the certificate request and aftewards by the server, is stored in a directory only accessible by root
. The directory with the certificates (/etc/ssl/lets/
) is owned by lets
who has only normal user privileges. Everybody has readonly access.
Certificate renewal
Since the certificate is only valid for 90 days, you must request a new one before expiration. You may use the same call of acme-tiny.py
like shown above. Since we are running OpenBSD it is advisable to write a script and run it using cron
.
The following script works with OpenBSD for other unix like operating systems you have to adapt the script. Let's call it renew_cert.sh
.
#!/bin/sh
# Renew certificates with Let's Encrypt
base="/home/lets"
alias acme_tiny="$base/bin/acme_tiny.py"
chaineddir="/etc/ssl/acme"
accountkey="$base/account.key"
reqsdir="$base/reqs"
certsdir="$base/certs"
challengedir="/var/www/challenges"
intermediate_cert="$certsdir/intermediate.pem"
intermediate_cert_url="https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
echo "Downloading intermediate certificate..."
# get intermediate certificate with curl
# -f: fail without writing html garbage to output file
# -s: silent mode without progress meter
# -S: output error message if download fails
curl -fsS $intermediate_cert_url -o $intermediate_cert && echo "Success."
# The script continues even if the download of the intermediate
# certificate fails. The old intermediate certificate should still
# exist and be useable.
for csr in $reqsdir/*.csr; do
domain=`basename $csr .csr`
echo "Requesting certificate for $domain..."
acme_tiny --account-key $accountkey --csr $csr --acme-dir $challengedir > $certsdir/$domain.crt || (echo "Failed!" ; exit)
echo "Success."
cat $certsdir/$domain.crt $intermediate_cert >$chaineddir/$domain-chained.pem
done
echo "Reloading web server configuration..."
doas /usr/sbin/rcctl reload nginx || (echo "Failed!"; exit)
echo "Success."
After renewal the web server must load its configurtion again. The user lets
should do this after renewing the certificate, but he needs the privileges for this. The can be done with a simple line in the doas
configuration file /etc/doas.conf
. The user lets
may now reload and only reload the web server configuration. For other operating systems you may achieve similar effects with the sudo configuration.
permit nopass lets as root cmd /usr/sbin/rcctl args reload nginx
cron
will now request a new certificate each month with the following crontab
. I like it when I receive mail from my script each time it runs.
MAILTO=cert@example.com 0 0 1 * * $HOME/bin/renew_certs.sh