Certbot worked fine the day you set it up. The site went green, the padlock showed up, and you moved on. Then three months later a customer emails a screenshot of "Your connection is not private" and you're staring at a server that was supposed to renew itself automatically. This is one of the most common tickets we see on unmanaged VPS boxes — the initial install goes smoothly, and the renewal quietly breaks without anyone noticing until the cert actually expires.
Symptom
A few different ways this shows up:
- Browser throws
NET::ERR_CERT_DATE_INVALIDon a site that had valid SSL a few weeks ago - Running
certbot renewmanually returns an error instead of "Congratulations, all renewals succeeded" certbot certificatesshowsVALID: 12 daysor less — it's about to expire and hasn't auto-renewed- You get a "Let's Encrypt certificate expiration notice" email from Let's Encrypt itself, which only fires when renewal has already been failing silently for a while
Start by checking the actual status instead of guessing:
sudo certbot certificates
sudo certbot renew --dry-run
The dry run doesn't touch your live certificate, but it runs the exact same validation Certbot would use for a real renewal — so whatever error it throws is your real problem.
Cause 1: The Renewal Job Isn't Actually Scheduled
Certbot installed via snap or the distro package usually sets up its own scheduler, but "usually" isn't "always" — especially if the server was migrated, cloned from a snapshot, or had its OS upgraded since. Check both places it could live:
systemctl list-timers | grep certbot
sudo crontab -l
cat /etc/cron.d/certbot 2>/dev/null
If none of those show a certbot entry, nothing has been trying to renew anything — the cert has just been counting down to expiry with no safety net. Re-enable the systemd timer with:
sudo systemctl enable --now snap.certbot.renew.timer
or, if you're not on the snap install, add a cron entry that runs twice a day (Certbot only actually renews within 30 days of expiry, so frequent checks are harmless):
0 3,15 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
Cause 2: Port 80 Got Blocked After the Fact
This is the one that catches people out the most. Let's Encrypt's default HTTP-01 challenge needs port 80 reachable from the public internet at the moment of renewal — not just at initial setup. If you tightened your firewall since then, locked down UFW/CSF rules, or moved traffic behind a load balancer that only forwards 443, renewal will time out with something like:
Challenge failed for domain example.com
Timeout during connect (likely firewall problem)
Confirm port 80 is actually open from outside, not just locally:
sudo ufw status | grep 80
curl -I http://example.com/.well-known/acme-challenge/test
If UFW or CSF is blocking it, allow it explicitly:
sudo ufw allow 80/tcp
# or in CSF, add 80 to TCP_IN in /etc/csf/csf.conf, then csf -r
If you genuinely can't leave port 80 open, switch that domain to the DNS-01 challenge instead, which validates through a TXT record rather than an HTTP request.
Cause 3: A Reverse Proxy or WAF Is Swallowing the Challenge Path
If Nginx sits in front of the app, or Cloudflare/a security plugin sits in front of Nginx, the /.well-known/acme-challenge/ path has to reach the filesystem unmodified. A catch-all redirect to HTTPS, a rewrite rule, or a WAF rule blocking "suspicious" long random-looking URLs will all break it silently. Check your Nginx config for a block like this before any HTTPS redirect:
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
And if the domain is proxied through Cloudflare (orange cloud), Let's Encrypt's validation server hits Cloudflare's IP first. For HTTP-01 to work through the proxy the challenge response has to actually pass through, which mostly works — but a Cloudflare page rule that forces HTTPS redirects for the whole zone can loop the challenge request before it ever reaches your origin. Temporarily set the domain to "DNS only" (grey cloud) during renewal if you suspect this, or move to DNS-01 validation permanently so proxying is a non-issue.
Cause 4: Rate Limits From Repeated Failed Attempts
Let's Encrypt caps how many certificates you can request for the same domain in a rolling window. If a cron job has been silently failing and retrying every few hours for weeks, or you've been testing config changes with real (not staging) requests, you can hit:
too many certificates already issued for this exact set of domains
Confirm this is actually your problem before doing anything else — the error message states it plainly, so don't assume. If you do need to test changes, use the staging environment, which has much higher limits and doesn't count against production:
sudo certbot renew --dry-run --staging
Real rate limits reset on a rolling weekly basis. There's no support ticket that speeds it up — you wait it out, or if it's genuinely urgent, install a paid certificate temporarily while you fix the underlying cause.
Cause 5: DNS Changed and the A Record No Longer Points Here
If the site moved servers, or you added a CDN, and the A record for this specific hostname (including any www or subdomain variants covered by the same cert) no longer resolves to this VPS, HTTP-01 validation will fail even though the site "works" through the CDN. Check every name on the certificate, not just the main one:
sudo certbot certificates
dig +short example.com
dig +short www.example.com
If any of them point somewhere else now, either update DNS back to this server for renewal purposes, drop that name from the cert with certbot certonly --cert-name example.com -d example.com,www.example.com (re-specifying only the names that still resolve here), or switch to DNS-01 validation so the A record doesn't matter.
Prevention
| What to do | Why it matters |
|---|---|
Run certbot renew --dry-run monthly, not just when something breaks | Catches firewall, WAF, or DNS drift before the real cert expires |
Add a --deploy-hook that reloads Nginx/Apache after every renewal | A renewed cert on disk does nothing until the web server picks it up |
| Monitor cert expiry separately from the server itself | Uptime monitoring won't catch a cert that's still valid but about to expire |
| Document any firewall or CDN changes that touch port 80 | Most renewal failures trace back to "we changed the firewall in March" |
| Prefer DNS-01 validation for wildcard certs or CDN-fronted domains | Removes the port-80-must-be-open dependency entirely |
Frequently Asked Questions
How do I know if my certificate will renew before it expires?
Run sudo certbot certificates to see the exact expiry date, then run sudo certbot renew --dry-run. If the dry run completes without errors, the real renewal will too. Certbot only attempts an actual renewal within 30 days of expiry, so a dry run outside that window still validates the challenge path even though it won't renew anything yet.
Does cPanel AutoSSL have the same problem?
AutoSSL runs its own renewal process inside cPanel/WHM and isn't affected by a missing cron job on the OS level, since WHM schedules it internally. It can still fail for similar reasons though — port 80 blocked, DNS pointed elsewhere, or a CAA record blocking the issuing CA — so the port and DNS checks in this guide still apply if AutoSSL is failing.
Can I switch to DNS-01 validation without losing my current certificate?
Yes. Your existing certificate stays valid until it expires; you're only changing how the next renewal proves domain ownership. Install a DNS plugin for your provider (e.g. certbot-dns-cloudflare) and re-run certbot with the --dns-cloudflare flag and the same domain names — it issues a fresh cert using the new method.
Why did renewal work last time but fail this time with no changes on my end?
Nothing changing on your end doesn't mean nothing changed. Common culprits are a Cloudflare setting toggled by someone else on the team, a security plugin update that added new WAF rules, or an OS update that reset a firewall config to defaults. Check curl -I against the challenge path first — that alone tells you if the request is even reaching your server.
Is it safe to just re-run the full certbot install command again?
It's safe, but it treats the symptom, not the cause. Re-running certbot --nginx -d example.com will usually succeed once and buy you 90 more days, but if the underlying issue (blocked port 80, DNS pointing elsewhere) isn't fixed, you'll be back here at the next renewal window. Diagnose with --dry-run first so you know what actually broke.
