Auto-renew letsencrypt certificates
Let's Encrypt is a great project that aims to increase security in the web by making it easy and cheap (free, in fact) to obtain SSL certificates. Part of their aim is to make sure web servers are configured correctly.
This article describes how to use acme_tiny.py, a small Python script that implements the ACME protocol to get a certificate for your domains. Please see the acme_tiny.py documentation on how to set it up and how to get the authentication key for the Let's Encrypt server.
Creating a key and a Certificate Signing Request
One cool thing about Let's Encrypt is that they allow more than one domain per certificate. You can add any number of domains in the SAN section. For this you need to enumerate all domains in the subjectAltName value in the [SAN] section of the openssl configuration file, e.g. this:
[SAN]
subjectAltName=DNS:example.com,DNS:www.example.com,DNS:example.net,DNS:www.example.net
The following command creates a secret key and a CSR. You'll need to change the values in the -subj option to your location and your details. Use the primary domain as the CN. If the -subj option is not used then openssl will query those values interactively.
openssl req \
-new -newkey rsa:2048 -sha256 -nodes \
-keyout privkey1.pem -out csr.pem \
-subj "/C=UK/ST=Some State/L=Some Place/O=example.com/emailAddress=webmaster@example.com/CN=example.com" \
-reqexts SAN
Submitting the CSR to the Let's Encrypt CA
The acme_tiny.py command needs the Let's Encrypt account key, the CSR and a webroot directory (acme-dir to submit the request). The webroot directory is the path on your file system to the top-level directory of your website, plus the directory .well-known/acme-challenge, in other words, the path that maps to the URL http://example.com/.well-known/acme-challenge on your server.
python /path/to/acme_tiny.py \
--account-key /path/to/account.key \
--csr csr.pem \
--acme-dir /path/to/acme_dir > /path/to/cert.pem
For each domain in the SAN list the acme_tiny.py script will store a file in the webroot directory to prove that you control that domain.
Note that each domain you submit must be accessible both from the internet and from the computer where the acme_tiny.py script is run.
Automating it all
The following script automates the process described above. To use the script, change the variables country, state, town and email and call it with a list of domains you want to include in the certificate, separated by space.
#!/bin/sh
#
# Wrapper script for the acme-tiny client to generate a server certificate in
# manual mode. It uses openssl to generate the key and does not modify the
# server configuration.
#
# Call once to create a private key and a CSR and a first certificate:
# letsencrypt.sh -r example.com,www.example.com,mx.example.com
#
# From then on, renew the certificate like this:
# letsencrypt.sh -u example.com
#
set -e
prog_name=`basename $0`
country=UK
state="Some State"
town="Some Place"
email=webmaster@example.com
usage() {
echo "$prog_name [OPTIONS]"
}
help() {
echo "$prog_name: generate or renew letsencrypt certificates"
echo
echo "OPTIONS"
echo " -h print this help message."
echo " -r DOMAINS register a list of domains, separated by comma."
echo " -u DOMAIN update the certificate for a domain."
}
primary_dom=
domains=
while getopts hr:u: opt; do
case "$opt" in
h) help
exit 0;;
r) domains="$OPTARG";;
u) primary_dom="$OPTARG";;
\?) # unknown flag
usage >&2
exit 1;;
esac
done
shift `expr $OPTIND - 1`
if [ $# -ne 0 ]; then
echo >&2 "$0: error: no arguments allowed."
exit 1
fi
if [ -z "$primary_dom" ]; then
primary_dom=`echo "$domains" | cut -d',' -f1`
fi
if [ -z "$primary_dom" ]; then
echo >&2 "$0: error: no primary domain. use -u"
exit 1
fi
account_key="/root/.config/letsencrypt/account.key"
challenge_dir="/var/www/$primary_dom/.well-known/acme-challenge"
csr="/etc/ssl/local/$primary_dom/csr.pem"
key="/etc/ssl/local/$primary_dom/privkey1.pem"
cert="/etc/ssl/local/$primary_dom/0000_cert.pem"
lev1="/etc/ssl/local/$primary_dom/0000_chain.pem"
chain="/etc/ssl/local/$primary_dom/0001_chain.pem"
if [ ! -f "$account_key" ]; then
echo >&2 "$0: error: no account key"
exit 1
fi
if [ ! -d "$challenge_dir" ]; then
echo >&2 "$0: error: no challenge dir for domain $primary_dom"
echo >&2 "$0: please create a .well-known/acme-challenge directory in your web space"
exit 1
fi
if [ ! -d `dirname "$csr"` ]; then
echo >&2 "$0: error: ssl directory `dirname "$csr"` does not exist"
exit 1
fi
tmpdir=
cleanup() {
if [ -n "$tmpdir" -a -d "$tmpdir" ]; then
rm -rf "$tmpdir"
fi
}
trap cleanup HUP INT QUIT TERM EXIT
tmpdir=`mktemp -d -t letsencrypt.XXXXXXXX`
tmp_key="$tmpdir/privkey1.pem"
tmp_crt="$tmpdir/cert.pem"
tmp_lev1="$tmpdir/chain.pem"
if [ -n "$domains" ]; then
domains=`echo $domains | sed -e 's/,/,DNS:/g'`
sslcnf="$tmpdir/openssl.cnf"
cat /etc/ssl/openssl.cnf > "$sslcnf"
echo "[SAN]" >> "$sslcnf"
echo "subjectAltName=DNS:$domains" >> "$sslcnf"
openssl req \
-new -newkey rsa:2048 -sha256 -nodes \
-keyout "$tmp_key" -out "$csr" \
-subj "/C=$country/ST=$state/L=$town/O=$primary_dom/emailAddress=$email/CN=$primary_dom" \
-reqexts "SAN" \
-config "$sslcnf"
mv -i "$tmp_key" "$key"
fi
if [ ! -f "$key" ]; then
echo >&2 "$0: error: no key for domain $primary_dom"
exit 1
fi
if [ ! -f "$csr" ]; then
echo >&2 "$0: error: no CSR for domain $primary_dom"
exit 1
fi
curl --silent https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt > "$tmp_lev1"
acme-tiny --quiet --account-key $account_key --csr $csr --acme-dir $challenge_dir > "$tmp_crt"
if [ -f "$tmp_key" ]; then
mv "$tmp_key" "$key"
fi
mv "$tmp_crt" "$cert"
mv "$tmp_lev1" "$lev1"
cat "$cert" "$lev1" > "$chain"
Updates
- 30 November 2016
- Fixed a couple of bugs when creating a key for a new domain.