All cluster nodes (thing-1, thing-2, etc.) store SSL certificates on the shared NFS mount at /mnt/ha-shared/letsencrypt. This ensures both nodes have access to the same certs, and any cert issued or renewed on either node is automatically available cluster-wide.
This is achieved by symlinking /etc/letsencrypt → /mnt/ha-shared/letsencrypt on each node. Certbot writes to /etc/letsencrypt as normal, but the data actually lands on shared storage.
Run the following on each cluster node that doesn't already have the symlink in place.
# Verify the current state — should NOT already be a symlink
ls -la /etc/letsencrypt
# Back up the existing local directory
sudo mv /etc/letsencrypt /etc/letsencrypt.bak
# Create the symlink
sudo ln -s /mnt/ha-shared/letsencrypt /etc/letsencrypt
# Verify the symlink
ls -la /etc/letsencrypt
Expected output after symlink:
lrwxrwxrwx 1 root root 27 Feb 25 22:00 /etc/letsencrypt -> /mnt/ha-shared/letsencrypt
Note: Make sure
/mnt/ha-sharedis mounted before certbot runs. If the NFS mount isn't up, certbot will fail or recreate/etc/letsencryptas a real directory.
Update DNS A record and www CNAME and watch until it migrates
watch -n 10 "dig +short A yourdomain.com @8.8.8.8"
sudo certbot certonly --webroot -w /mnt/ha-shared/acme-challenge -d yourdomain.com
ls -la /mnt/ha-shared/letsencrypt/live/your.domain.com/
You should see fullchain.pem, privkey.pem, cert.pem, and chain.pem.
Reference the cert paths in your nginx config (these paths work from either node since they resolve through the symlink):
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
sudo systemctl restart ha-nginx-proxy.service
Renewals work the same way. On either node, run:
sudo certbot renew
Because /etc/letsencrypt symlinks to the shared NFS mount, the renewal configs and certs are consistent across nodes. You only need to run renewal on one node — the updated certs will be immediately available on both.
To test renewal without actually renewing:
sudo certbot renew --dry-run
Root crontab on both thing-1 and thing-2 (sudo crontab -e):
0 3 * * * [ -d /mnt/ha-shared/letsencrypt ] && /usr/bin/certbot renew 2>&1 | /usr/bin/logger -t certbot
0 8 * * * /usr/local/bin/cert-check.sh
[ -d /mnt/ha-shared/letsencrypt ] ensures only the active Pacemaker node runs certbotjournalctl -t certbot/usr/local/bin/cert-check.sh runs at 8am daily. Logs a warning to syslog for any cert expiring within 14 days:
#!/bin/bash
[ -d /mnt/ha-shared/letsencrypt/live ] || exit 0
for f in /mnt/ha-shared/letsencrypt/live/*/cert.pem; do
d=$(basename $(dirname $f))
exp=$(openssl x509 -enddate -noout -in $f | cut -d= -f2)
days=$(( ($(date -d "$exp" +%s) - $(date +%s)) / 86400 ))
[ $days -lt 14 ] && logger -t cert-check "WARNING: $d expires in $days days"
done
Check warnings with:
journalctl -t cert-check
All configs in /mnt/ha-shared/letsencrypt/renewal/ must use webroot authenticator:
version = 0.39.0
archive_dir = /etc/letsencrypt/archive/example.com
cert = /etc/letsencrypt/live/example.com/cert.pem
privkey = /etc/letsencrypt/live/example.com/privkey.pem
chain = /etc/letsencrypt/live/example.com/chain.pem
fullchain = /etc/letsencrypt/live/example.com/fullchain.pem
[renewalparams]
account = 259099f3be305925678dd9518c8f3029
authenticator = webroot
webroot_path = /mnt/ha-shared/acme-challenge,
server = https://acme-v02.api.letsencrypt.org/directory
[[webroot_map]]
example.com = /mnt/ha-shared/acme-challenge
| Problem | Fix |
|---|---|
/etc/letsencrypt is a real directory (not a symlink) |
sudo mv /etc/letsencrypt /etc/letsencrypt.bak && sudo ln -s /mnt/ha-shared/letsencrypt /etc/letsencrypt |
| Certbot says no renewal configs found | The symlink may be broken — check that NFS is mounted: ls /mnt/ha-shared/ |
| DNS TXT record not verifying | Wait longer for propagation, or use a DNS checker to confirm the record is live before pressing Enter |
Cert saved to /etc/letsencrypt but symlink isn't set up |
Copy manually: sudo cp -r /etc/letsencrypt/live/domain /mnt/ha-shared/letsencrypt/live/ and repeat for archive/ and renewal/ |