You issued the cert months ago. certbot certificates still lists it. Nobody has run certbot renew since.
The leaf on :443 is inside the last 30 days. Or it already expired.
There is no failed challenge in today's log. There is no "renewed but nginx ignored it" story. Nothing scheduled the renew. That is this post.
On most Debian/Ubuntu installs, the systemd unit certbot.timer is what calls certbot.service twice a day. If the timer is inactive, masked, or missing, disk and wire both age together until something external notices — which is how letsencrypt auto renew stops without a single error.
Prove it
1. Is the timer even there?
systemctl status certbot.timer systemctl is-enabled certbot.timer systemctl list-timers --all | grep -i certbot
What you want:
Active: active (waiting)(or a similar "waiting" state)enabledfromis-enabled- A next trigger time in
list-timers
What means renew is not scheduled:
| Result | Meaning |
|---|---|
Unit certbot.timer could not be found | The package never installed the unit, or wrong machine |
inactive (dead) / disabled | The unit exists; nothing starts it on boot |
masked | Someone blocked it on purpose (or a broken cleanup) |
Timer active, but list-timers shows no next run | Broken unit or calendar; inspect the unit file |
| No certbot lines; only a cron you never wrote | You are on cron-based renew (see below) |
2. When did renew last run?
# systemd journal for the renew service sudo journalctl -u certbot.service -n 50 --no-pager # classic log sudo tail -n 40 /var/log/letsencrypt/letsencrypt.log # lineaged cert ages on disk sudo certbot certificates
If the log's last real renew is weeks or months old, and the timer is dead, you found the gap.
If the log shows recent failed HTTP-01 or DNS-01 attempts, the timer is running. Fix the challenge instead: HTTP-01 died after you forced HTTPS.
If the log shows recent success but browsers still see an old leaf, that is reload, not scheduling: nginx reloaded nothing. Certbot still exited 0.
3. What is on the wire right now?
echo | openssl s_client -connect yourdomain.com:443 \ -servername yourdomain.com 2>/dev/null \ | openssl x509 -noout -enddate -subject
Or paste the hostname into the SSL expiry checker. Read days left on the served leaf. That is the clock visitors feel while the timer stays off.
For the full failure map (reload, HTTP-01, rate limit, root mail), use the disk vs wire hub.
Fix now
systemd (Debian / Ubuntu package path)
# if masked, clear that first sudo systemctl unmask certbot.timer sudo systemctl enable --now certbot.timer systemctl status certbot.timer systemctl list-timers | grep -i certbot
Optional: run one renew immediately so you are not waiting on the next timer tick:
sudo certbot renew --dry-run # when the dry-run is clean: sudo certbot renew
Then confirm disk notAfter moved (when the cert was inside the renew window) and reload TLS if your stack needs it — that half is the reload write-up.
Package missing the timer unit
# Debian/Ubuntu dpkg -L certbot | grep -E 'timer|service' || true apt-cache policy certbot
If the package is absent or ancient, install or refresh it from your distro docs. Snap-based certbot uses a different scheduler (below). Do not invent a second parallel cron and a timer without knowing which one owns renew. One scheduler is enough.
cron-based installs (older guides, some AMIs)
sudo ls -la /etc/cron.d/ /etc/cron.daily/ 2>/dev/null | grep -i certbot || true sudo grep -R certbot /etc/cron.* /var/spool/cron 2>/dev/null | head
A healthy cron line usually looks like a twice-daily certbot renew -q (exact flags vary). If the file is missing, commented out, or points at a binary that no longer exists, restore it from your distro's certbot package or from Certbot's current docs for your OS, then:
sudo certbot renew --dry-run
snap certbot
snap list certbot 2>/dev/null || true systemctl list-timers --all | grep -i snap.certbot || true
snap often installs its own renew timer; the name varies by version. If you migrated from apt to snap (or the reverse), you can end up with no active timer on either side. Pick one install method, remove the orphaned one, enable that method's timer.
Containers / immutable hosts
There is no durable certbot.timer on a container that was only used once to issue. Renew must live on:
- the host that still has
/etc/letsencrypt, or - your orchestrator's CronJob / scheduled task, or
- DNS-01 in CI with a written schedule
If "the box" was replaced and the new image never got a timer, wire will keep the old leaf until someone issues again. Fix scheduling on the long-lived place that holds the lineage, then reload every edge that terminates TLS.
Make it permanent
- Alert on timer health, not only on cert expiry: a check that
systemctl is-active certbot.timeris active, or thatlist-timersstill shows a next run. - Keep
certbot.timerin config management so a "cleanup" playbook cannot leave it disabled. - After OS upgrades, re-run
systemctl is-enabled certbot.timeronce. Unit renames and package switches do happen. - Prefer off-box expiry on the public hostname, so a dead timer is visible even when root mail never leaves the box.
How you catch it off-box
A dead timer produces silence. No failed challenge. No deploy-hook drama. Local logs look calm if nobody opens them.
- One-off: SSL expiry checker on the public name. See days left while you fix scheduling.
- Any port, quick wire read: SSL checker.
- Ongoing: CertPost watches the served cert (3 free, no card) and emails at 30/14/7/1 days. When the timer dies again after a rebuild, the leaf still ages in public and the alert still fires.
Watch the wire. Put renew back on a clock.
Related failures
- Certbot says it renewed. Your server is still serving the old cert. — the full failure map
- nginx reloaded nothing. Certbot still exited 0. — the timer ran; the process never reloaded
- HTTP-01 died after you forced HTTPS. — the timer ran; the challenge was blocked
- Renewal failed and root mail never left the box (next write-up in this series)
- Full chain check when leaf dates look fine but clients still warn
