#!/bin/sh # One pass of the loop: make the certificate on the volume the one CERTBOT_DOMAIN # asks for, then hand it to the running HAProxy. Every pass, not just the first. # # `certbot certonly` rather than `certbot renew`, and that is the whole reason # this script is the pass: renew takes the names from the certificate it already # holds and never reads CERTBOT_DOMAIN, so an edit to the variable stayed # invisible until somebody issued the new set by hand. certonly is told the names # on every pass, so the certificate follows the variable in both directions. # # --keep-until-expiring is what makes calling it twice a day free: with the same # names and no expiry due, certbot answers "Certificate not yet due for renewal; # no action taken" without opening a connection, so nothing is spent against # Let's Encrypt's rate limits. set -e if [ ! -f /etc/certificates/site.pem ]; then # Self-signed placeholder, so HAProxy can bind :443 before a real certificate # exists. Built inside a subshell: this script is sourced, so a bare cd or # umask would change the caller's shell too. Relative paths used to resolve # against the working directory (/opt/certbot) and left the private key there # for good; here they resolve against the volume the key belongs on, and the # intermediates are removed before site.pem appears. ( umask 077 cd /etc/certificates openssl req -x509 -newkey rsa:2048 -nodes -days 365 -batch \ -subj "/CN=${CERTBOT_DOMAIN:-localhost}" \ -keyout site.key.tmp -out site.crt.tmp cat site.key.tmp site.crt.tmp > site.pem.tmp rm -f site.key.tmp site.crt.tmp # Readable by HAProxy, which runs unprivileged (uid 1001) and opens this # file while parsing `bind ... ssl crt`. Under the umask above the # placeholder came out 0600 root-only, HAProxy could not start at all, and # the pass then waited forever for a runtime API socket that no longer had # anyone to create it — a failed issuance took the whole site down instead # of leaving it on the placeholder. The real certificate has been 0644 all # along: 2-concatenate-cert.sh writes it with the default umask. Set on the # temporary file, so the rename below stays the only thing HAProxy can see. chmod 644 site.pem.tmp # Last and atomic: HAProxy reads site.pem at start-up and must never find # it half-written. mv site.pem.tmp site.pem ) fi # Wait for haproxy: Let's Encrypt reaches this container's challenge server only # through it, so asking for a certificate first would just fail validation. The # runtime API socket appearing is the signal — HAProxy creates it once the # configuration has parsed and the binds have succeeded. Waiting on the socket # rather than a TCP probe also means there is no port left to probe. HAPROXY_SOCKET=/var/lib/haproxy/admin.sock while [ ! -S "$HAPROXY_SOCKET" ]; do echo "Waiting for $HAPROXY_SOCKET..." sleep 7 done if [ -n "$CERTBOT_DOMAIN" ]; then # E-mail for Let's Encrypt expiry notices. Built with set -- so that the flag # and its value stay two separate arguments: a single "$CERTBOT_OPTS" word # reached certbot as one token ("--email you@example.org") and was rejected # as an unknown argument, which failed every issuance with an address set. # The entrypoint passes no positional parameters, so there are none to lose. if [ -n "$CERTBOT_EMAIL" ]; then set -- --email "$CERTBOT_EMAIL" else set -- --register-unsafely-without-email fi # Request certificate. --cert-name pins the lineage to the first name, so a # changed list updates that certificate instead of starting a second one # beside it; with the lineage named, certbot needs no --expand to accept an # added name non-interactively. # # --logs-dir moves certbot's log out of /var/log on the root filesystem and # into the volume the rest of its state already lives in. Two things follow. # The container can be run with `read_only: true`, which the log was the only # thing standing in the way of. And the log of a renewal that failed outlives # the container, which matters here more than anywhere else: this is the one # service whose breakage surfaces sixty days later, and the log is what says # why. certbot certonly --standalone \ --non-interactive --agree-tos --http-01-port=380 \ --logs-dir /etc/letsencrypt/logs \ "$@" \ --cert-name "${CERTBOT_DOMAIN%%,*}" \ --keep-until-expiring \ -d "$CERTBOT_DOMAIN" # Concatenate certificates . $(dirname $0)/2-concatenate-cert.sh fi # Update certificates in HAProxy . $(dirname $0)/3-update-haproxy-cert.sh