WhyOne wildcard certificate, obtained from Let’s Encrypt and renewed automatically, serves every TLS service on the host — web, mail, IMAP, LDAP, XMPP, database. The renewal runs as an unprivileged user with a DNS key that can write exactly one record, and services are reloaded by a root cron job the renewal user can only signal, never command.
Design
Validation uses the dns-01 challenge, which works for
wildcard names and needs no service on port 80. The naive form —
letting the ACME client update the production zones — would hand
an automated, internet-facing process a key to every zone. Instead the
challenges live in a dedicated dynamic zone, and every real
domain delegates only its challenge name into it with a
CNAME. The production zones stay static files; the update
key can create one TXT record in one throwaway zone and nothing
else.
DNS side
The challenge zone is dynamic, and its update policy grants the key a single name, a single type:
zone "Challenge zone" { type master; file "dynamic/Challenge zone"; update-policy { grant "cert-update" name _acme-challenge.Challenge zone TXT; }; }; include "cert-update.key";
Each certificate domain points its challenge name into that zone — one line per zone file, and the only ACME-related line the static zones ever carry:
_acme-challenge IN CNAME _acme-challenge.Challenge zone.
The TSIG key is generated with tsig-keygen cert-update.
Two readers need it and root is neither: the
named daemon re-reads it on every
rndc reload, and the ACME user presents it through
nsupdate. Owner for one, group for the other, nothing for
anyone else:
chown bind:ACME user cert-update.key
chmod 640 cert-update.key
Client side
The client is acme.sh, running as a dedicated
unprivileged user whose home doubles as the certificate store
(/var/db/acme, mode 750). Its account
configuration names the dns_nsupdate validation hook and
the pieces above:
NSUPDATE_SERVER='Primary nameserver' NSUPDATE_KEY='/usr/local/etc/namedb/cert-update.key' NSUPDATE_ZONE='Challenge zone'
Issuing then names every domain and SAN the certificate must carry;
installation copies the result to the shared certificate location the
services read (see the Apache page for
the key’s group-based permissions, which survive renewal because
acme.sh writes through the existing file):
acme.sh --issue --dns dns_nsupdate -d Domain -d 'Wildcard' acme.sh --install-cert -d Domain \ --cert-file /usr/local/etc/cert/letsencrypt/ALL.Domain.crt \ --key-file /usr/local/etc/cert/letsencrypt/ALL.Domain.key \ --ca-file /usr/local/etc/cert/letsencrypt/ca.pem \ --fullchain-file /usr/local/etc/cert/letsencrypt/fullchain.pem \ --reloadcmd "touch /var/db/acme/cert-renewed"
Renewal and service reload
A daily cron entry, owned by the ACME user, renews whatever is due:
17 22 * * * /usr/local/sbin/acme.sh --cron --home /var/db/acme/.acme.sh > /dev/null
The unprivileged user cannot — and should not — restart
the services that hold the old certificate in memory. The
reloadcmd above only drops a watchfile inside the ACME
user’s own directory; a root cron job checks for it and does
the restarts. The watchfile must not live in a world-writable place
like /tmp, where any local user could create
it and trigger the nightly restart of every TLS service.
0 23 * * * /root/cron/reload-cert-services
#!/bin/sh
WATCHFILE=/var/db/acme/cert-renewed
if [ -e "$WATCHFILE" ]; then
service apache24 reload
service postfix restart
# ... every service reading the shared certificate ...
unlink -- "$WATCHFILE"
fi