Certbot finished the renew. The log line says success. /etc/letsencrypt/live/yourdomain.com/fullchain.pem has a notAfter about 89 days out.
openssl s_client against :443 still prints last month's date.
nginx loads certificates at start (and on reload). A successful renew on disk does nothing for visitors until something runs reload. That gap is this whole post.
Prove it (disk vs wire)
Run both. Compare the two end dates.
# disk — what certbot wrote openssl x509 -noout -enddate \ -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem # wire — what visitors get on :443 echo | openssl s_client -connect yourdomain.com:443 \ -servername yourdomain.com 2>/dev/null \ | openssl x509 -noout -enddate
If disk is fresh and wire is old, stop debugging ACME. The issue is reload (or the wrong server block / wrong cert path).
Same answer without SSH: paste the hostname into the free SSL check. Read the served notAfter. That is the wire date.
For the full failure map (timer, HTTP-01, rate limit, root mail), use the disk vs wire hub.
Fix now
Reload the process that terminates TLS for that name.
# systemd nginx sudo nginx -t && sudo systemctl reload nginx # or sudo nginx -t && sudo nginx -s reload
Re-run the wire command (or the free check). The served notAfter should match disk.
If wire is still old after a clean reload:
- Confirm which binary owns
:443(ss -lptn 'sport = :443'orsudo lsof -i :443). - Confirm the
ssl_certificatepath in the server block points at the samefullchain.pemyou inspected. - If you terminate TLS on a load balancer or another host, reload that edge, not only the app box.
Make it permanent (deploy hook)
Certbot runs scripts in /etc/letsencrypt/renewal-hooks/deploy/ after a successful renew. Put the reload there so the next renew does not depend on memory.
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'HOOK' #!/bin/sh # Reload nginx only when certbot renewed at least one cert. systemctl reload nginx HOOK sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Dry-run does not always exercise deploy hooks the way a real renew does. After the next real renew (or a controlled renew in a maintenance window), compare disk and wire again.
Optional: only reload when the renewed lineage matches hosts this box serves. Keep the first version simple: one reload after any successful renew on this machine is enough for most single-nginx boxes.
How you catch it off-box
From inside the server, every log can still say "renewed." From outside, the served cert is the only signal that matches the browser.
- One-off: SSL checker on the public hostname.
- Expiry-focused read: SSL expiry checker.
- Ongoing: CertPost watches the served cert (3 free, no card) and emails at 30/14/7/1 days, plus chain and hostname breaks.
Watch the wire. Not only the renew log.
Related failures
- Certbot says it renewed. Your server is still serving the old cert. — the full disk-vs-wire failure map: challenge, timer, mail, rate limits
- HTTP-01 died after you forced HTTPS. — when the renew itself fails because a redirect ate the challenge
- The certbot timer is not running. — when nothing schedules the renew at all
- Your certificate renewed. Your visitors still got the old one. — a postmortem of this exact failure in the wild
- Full chain check when disk dates match but browsers still warn
