CODEX // oaoisme wiki

§ Host operations — keeping the box alive

Renewed certs that never get served — the certbot deploy hook

updated 2026-07-11

The wildcard *.oaoisme.top certificate renews automatically. But a quiet gap meant a freshly renewed cert would have sat on disk unused — nginx would keep serving the old one until something reloaded it by hand.

The gap

Renewal here uses a DNS-01 manual hook (Namecheap) — see the cert/DNS notes:

# /etc/letsencrypt/renewal/oaoisme.top.conf
authenticator = manual
manual_auth_hook   = /usr/local/bin/namecheap-dns-hook.py auth
manual_cleanup_hook = /usr/local/bin/namecheap-dns-hook.py cleanup

Those hooks handle issuance (prove domain control via a TXT record). Nothing in that path tells nginx to pick up the new certificate. And the deploy-hook directory was empty:

ls /etc/letsencrypt/renewal-hooks/deploy/   # → (empty)

So the sequence after a successful certbot renew would have been: new fullchain.pem written → nginx still holding the old cert in memory → clients served the old cert until the next unrelated reload or reboot. For a cert that renews roughly every 60 days, that window is not hypothetical.

The fix: a deploy hook that reloads nginx

certbot runs everything in renewal-hooks/deploy/ after each successful renewal, once per renewed lineage, with $RENEWED_LINEAGE set. Validate before reloading so a bad edit can never take nginx down on renewal:

# /etc/letsencrypt/renewal-hooks/deploy/10-reload-nginx.sh   (chmod 755)
#!/bin/sh
if nginx -t 2>/dev/null; then
    systemctl reload nginx
    echo "deploy-hook: reloaded nginx after cert renewal ($RENEWED_LINEAGE)"
else
    echo "deploy-hook: nginx -t FAILED, skipped reload" >&2
    exit 1
fi

Test it without waiting 60 days for a real renewal:

RENEWED_LINEAGE=/etc/letsencrypt/live/oaoisme.top \
  /etc/letsencrypt/renewal-hooks/deploy/10-reload-nginx.sh
# → deploy-hook: reloaded nginx after cert renewal (/etc/letsencrypt/live/oaoisme.top)

deploy vs pre vs post

renewal-hooks/ has three sibling dirs and they fire at different times:

  • pre/ — before every renewal attempt (even if nothing renews).
  • post/ — after every attempt (even if nothing renewed).
  • deploy/only when a cert was actually renewed, per lineage. This is the one that should reload services — it won't churn nginx on the many no-op certbot.timer runs that renew nothing.

Generalise it

Any service that caches a certificate in memory (nginx, most reverse proxies, mail servers) needs a deploy hook to reload it after renewal — issuance and service-reload are separate concerns, and the DNS-01/manual path only covers the first. If you use certbot --nginx, the installer handles reload for you; with authenticator = manual (as here) it does not, and the empty deploy/ dir is a silent trap.