Nova Notes

Zero-downtime certificate rotation with nginx

· Clara Lehto · 5 min read

Ninety-day certificates are great for security and terrible for anyone who renews them by hand. Here is the setup I use on every small server: certbot renews in the background, a deploy hook validates the configuration, and nginx picks up the new certificate without dropping connections.

Webroot instead of standalone

The standalone authenticator wants port 80 for itself, which means stopping nginx during renewal. The webroot method lets nginx keep serving and just exposes the challenge directory:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/acme;
    default_type "text/plain";
}
certbot certonly --webroot -w /var/www/acme -d example.org

A deploy hook that cannot break production

Certbot runs scripts from /etc/letsencrypt/renewal-hooks/deploy/ only when a certificate was actually renewed. Mine refuses to reload if the configuration is invalid:

#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/nginx-reload
set -e
nginx -t -q
nginx -s reload
logger -t certbot "reloaded nginx for $RENEWED_DOMAINS"

A reload starts new worker processes with the new certificate while old workers finish their current requests. Clients never see a reset.

Session tickets and OCSP

Two details are worth checking after rotation. If you use session tickets with a static key file, rotate that too — or simply disable tickets and rely on the session cache. And if you enabled OCSP stapling, the first handshakes after a reload will not have a stapled response yet; that is harmless, but it surprises people looking at openssl s_client output.

Monitoring expiry anyway

Automation fails silently. A DNS change, a firewall rule or a moved webroot can break renewal months before anyone notices. I keep an external check that alerts when any certificate has fewer than 14 days left:

echo | openssl s_client -connect example.org:443 -servername example.org 2>/dev/null \
  | openssl x509 -noout -enddate
Renewal is not done when it works once. It is done when you get an alert the day it stops working.