3

The certbot command provides two hooks that run after automated renewals, from the docs:

 --post-hook POST_HOOK
                       Command to be run in a shell after attempting to
                       obtain/renew certificates. Can be used to deploy
                       renewed certificates, or to restart any servers that
                       were stopped by --pre-hook. This is only run if an
                       attempt was made to obtain/renew a certificate. If
                       multiple renewed certificates have identical post-
                       hooks, only one will be run. (default: None)
 --deploy-hook DEPLOY_HOOK
                       Command to be run in a shell once for each
                       successfully issued certificate. For this command, the
                       shell variable $RENEWED_LINEAGE will point to the
                       config live subdirectory (for example,
                       "/etc/letsencrypt/live/example.com") containing the
                       new certificates and keys; the shell variable
                       $RENEWED_DOMAINS will contain a space-delimited list
                       of renewed certificate domains (for example,
                       "example.com www.example.com" (default: None)

This issue is outlined in this (now closed) LE thread and is basically about minimising interruption to services. POST_HOOK executes every time an attempt to renew is made even if no certificates were issued, though only once. This makes it possible to unnecessarily restart services. DEPLOY_HOOK runs for each and every successful certificate renewal. If one uses DEPLOY_HOOK, and has multiple certificates, each service may restart multiple times when once is enough. More info on renewal hooks here.

I use an issuance method that does not interrupt my services at all, e.g.:

certbot certonly --webroot ...

or

certbot certonly --dns-PROVIDER ...

I want to restart/reload each dependent service only once, and only if its certificate actually changed.

Walf
  • 401
  • 1
  • 6
  • 17

1 Answers1

3

I was able to overcome this limitation by using both hooks in combination with a simple script. Instead of each successful renewal immediately triggering a reload/restart, its --deploy-hook/renew_hook only marks the service for an action (by creating temporary files under /run), then when the --post-hook/post_hook runs, it checks for marked services and performs the necessary actions only once. This guarantees that nothing happens to a service unless a certificate it depends on actually changed, and it only happens once.

For example, to reload nginx, and restart vsftpd when a certificate they share is renewed:

certbot certonly ... \
    --deploy-hook '/usr/local/sbin/read-new-certs-services nginx --restart vsftpd' \
    --post-hook /usr/local/sbin/read-new-certs-services

Another certificate may be web-only, so it would have these hook parameters:

certbot certonly ... \
    --deploy-hook '/usr/local/sbin/read-new-certs-services nginx' \
    --post-hook /usr/local/sbin/read-new-certs-services

The script /usr/local/sbin/read-new-certs-services is this:

#!/bin/sh

run_base=/run
dir_reloads="$run_base/new-cert-reloads"
dir_restarts="$run_base/new-cert-restarts"

if [ $# -gt 0 ]; then
    some=0
    dir="$dir_reloads"
    while [ $# -gt 0 ]; do
        case $1 in
            -h|--help)
                >&2 cat <<-'EOHELP'
                    Usage:

                      read-new-certs-services [-l|-s] service1 [[-l|-s] service2]
                      or
                      read-new-certs-services

                      When called without arguments, marked services will be reloaded or restarted.

                    Arguments:

                      -h, --help
                        This help.

                      -l, --reload
                        Mark all subsequently listed services for reloading. This is the default.

                      -s, --restart
                        Mark all subsequently listed services for restarting.
                    EOHELP
                exit
                ;;
            -l|--reload)
                dir="$dir_reloads"
                ;;
            -s|--restart)
                dir="$dir_restarts"
                ;;
            *)
                if [ -n "$1" ]; then
                    some=1
                    run_file="$dir/$1"
                    if [ ! -f "$run_file" ] && ! install -D /dev/null "$run_file"; then
                        >&2 echo "Service could not be marked: $run_file"
                        exit 1
                    fi
                fi
                ;;
        esac
        shift
    done
    if [ $some -eq 0 ]; then
        >&2 echo 'No service(s) specified.'
        exit 1
    fi
    exit
fi

if [ -d "$dir_restarts" ]; then
    find "$dir_restarts" -mindepth 1 -printf '%P\t%p\n' | while IFS="$(printf '\t')" read -r svc run_file; do
        systemctl restart "$svc" && rm "$run_file"
        # no need to reload as well if restarting
        run_file="$dir_reloads/$svc"
        if [ -f "$run_file" ]; then
            rm "$run_file"
        fi
    done
fi

if [ -d "$dir_reloads" ]; then
    find "$dir_reloads" -mindepth 1 -printf '%P\t%p\n' | while IFS="$(printf '\t')" read -r svc run_file; do
        systemctl reload "$svc" && rm "$run_file"
    done
fi

This script must be used on both hooks for every certificate issue so that none use a conflicting method of restarting services.

You can change existing certificate renewals to use this method by editing their /etc/letsencrypt/renewal/*.conf files to contain hooks like this in the [renewalparams] section:

renew_hook = /usr/local/sbin/read-new-certs-services nginx -s vsftpd
post_hook = /usr/local/sbin/read-new-certs-services
Walf
  • 401
  • 1
  • 6
  • 17