Why Certbot did not renew
The certificate on the wire is what users hit. Certbot logs are what operators read. They diverge more often than people expect.
Certbot renews when its timer runs, the challenge succeeds, and the web server actually loads the new files. If any of those fail, the live handshake still shows the old notAfter. Check that first at sslert.com/check.
1. The timer never ran
Debian/Ubuntu install a systemd timer (certbot.timer). Snap installs a different one. If the timer is disabled, the host was down at 00:00, or you only ran certbot certonly once by hand, nothing renews. systemctl list-timers and journalctl -u certbot.service are the first two commands.
2. nginx (or Caddy, or Apache) was not reloaded
Certbot can write new PEMs under /etc/letsencrypt/live/… and still leave the old certificate in the process memory. A missing deploy hook, a failed nginx -s reload, or a unit that points at a copied file instead of the symlink all produce “renewal succeeded” in the log and an old cert on port 443.
3. Mixed chain / wrong file
The vhost ssl_certificate must be the full chain the client needs. Serving only the leaf, or a leftover fullchain.pem from another lineage, looks fine in openssl x509 -in on disk and fails (or shows the wrong issuer) in a live handshake.
4. DNS-01 never completed
HTTP-01 needs port 80 reachable on the exact name. DNS-01 needs the API token still valid and the TXT record visible on the nameservers Certbot queries — not only in the registrar UI. Wildcards cannot use HTTP-01. A stale Cloudflare token is a common silent skip.
5. Expired ACME account key
The Let’s Encrypt account key under /etc/letsencrypt/accounts/ is not the certificate. If that directory was copied from a dead host, or the account was deactivated, every renew returns an ACME error and the existing cert keeps ageing.
6. Staging CA
--staging or acme-staging-v02.api.letsencrypt.org issues certificates that browsers do not trust. Staging is for rate-limit testing. A leftover staging lineage will “renew” forever and still show as untrusted in a real handshake.
7. Disk
A full /var or a root filesystem at 100% lets Certbot fail while writing the new lineage. The previous files remain. The timer looks healthy. The cert on the balancer is still the old one.
SSLert does not run Certbot. It does a TLS handshake against the hostname you give it, which is the question users actually need answered: what is being served right now?