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_INVALID on a site that had valid SSL a few weeks ago
  • Running certbot renew manually returns an error instead of "Congratulations, all renewals succeeded"
  • certbot certificates shows VALID: 12 days or 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 doWhy it matters
Run certbot renew --dry-run monthly, not just when something breaksCatches firewall, WAF, or DNS drift before the real cert expires
Add a --deploy-hook that reloads Nginx/Apache after every renewalA renewed cert on disk does nothing until the web server picks it up
Monitor cert expiry separately from the server itselfUptime monitoring won't catch a cert that's still valid but about to expire
Document any firewall or CDN changes that touch port 80Most renewal failures trace back to "we changed the firewall in March"
Prefer DNS-01 validation for wildcard certs or CDN-fronted domainsRemoves 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.