Shami's Blog

Sysadmin, Because Even Developers Need Heroes

Letsencrypt Pre-renew Hooks

2021-07-17

Acmetool used to be my go-to tool for LetsEncrypt. It was quick and simple to set up. As a user, my favorite part of the Golang ecosystem is that binary files are statically linked. You don’t have to fiddle with any dependencies. But even though Acmetool is still getting occasional updates the last release is from 2018 and I prefer to stick to releases. There were times when Acmetool would not work behind Cloudflare and I would have to temporarily disable Cloudflare proxying to be able to generate certificates.

Another alternative was lego which is a library and command line client for the ACME protocol. I read that it was a very solid tool but what kept me from using it was that it can only manage one certificate at a time. In 2019 I managed servers with tens of websites and Acmetool didn’t work so I needed a few crontab entries to manage them with lego.

I wrote a couple of wrapper scripts for lego and that made it my favorite ACME client. And while trying to figure out the setup I mentioned in my previous article I read some posts asking for pre-renewal hooks and I realized my scripts enable that, so will be sharing them below:

The new certificate generation script:

#!/bin/sh

# Do not tolerate errors
set -e

. /root/.lego

if [ -z $PORT ]; then
    request="webroot /usr/local/www"
else
    request="port $PORT"
fi

requestCert() {
    $LEGO \
        --accept-tos \
        --path $LEGODIR \
        --http \
        --http.$request \
        --email $EMAIL \
        --domains $1 \
        --pem \
        run
}

LEGO=/usr/local/bin/lego
LEGODIR=/var/db/lego

domainsParam="$1"
shift

for domain in $*
do
    domainsParam="$domainsParam --domains $domain"
done

requestCert "$domainsParam"

The contents of /root/.lego

[email protected]       # Mandatory
PORT=127.0.0.1:402      # Optional, if not specified the acme challenge is stored in /usr/local/www/.well-known/acme-challenge, if specified, lego will listen on the defined IP and port

You will have to configure your web server or load balancer to use either the folder or the proxy to the port. I will not go into detail here as this is thoroughly documented online.

Usage: acmenew domain1 [domain2 domain3 ...]

This will generate the following set of files in /var/db/lego/certificates/

domain1.crt
domain1.issuer.crt
domain1.json
domain1.key
domain1.pem

Now for the renewal script:

#!/bin/sh

# Do not tolerate errors
set -e

. /root/.lego

if [ -z $PORT ]; then
    request="webroot /usr/local/www"
else
    request="port $PORT"
fi

renewCert() {
    # If we have a hook script in /root/.hooks, add the --renew-hook argument
    hook=""
    if [ -f /root/.hooks/$1 ]; then
        hook="--renew-hook /root/.hooks/$1"
    fi
    $LEGO \
        --accept-tos \
        --path $LEGODIR \
        --http \
        --http.$request \
        --email $EMAIL \
        --domains $1 \
        --pem \
        renew --days $DAYS ${hook}
}

# Do not attempt renewal if the certificate has more than X days available
# 86400 seconds is 1 day
DAYS=22
EXPIRSIN=`expr $DAYS \* 86400`

LEGO=/usr/local/bin/lego
LEGODIR=/var/db/lego
OPENSSL=/usr/bin/openssl

FIND=/usr/bin/find
SED=/usr/bin/sed
BASENAME=/usr/bin/basename

# Loop through current certificates
CERTS=`$FIND $LEGODIR -name '*crt' -not -name '*issuer*' -type f`

for cert in $CERTS
do
    # If the certificate expires in the period mentioned above, renew it
    $OPENSSL x509 -checkend $EXPIRSIN -noout -in $cert -out /dev/null ||
        renewCert `$BASENAME $cert | $SED 's/.crt//'`
done

This script does the following:

  1. Loop through all certificates in /var/db/lego/certificates
  2. Use openssl to check the certificate expiry date
  3. Run renewCert only when a certificate is about to expire
  4. Look for /root/.hooks/domainname, if that exists, instruct lego to run it after renewing the certificate

To add a pre-hook, all you need is to modify the beginning of renewCert

About Me

Dev gone Ops gone DevOps. Any views expressed on this blog are mine alone and do not necessarily reflect the views of my employer.

twitter linkedin