Fixing certbot Renewal Failures: A Cause-by-Cause Troubleshooting Guide for certificate expired
If a Your certificate (...) will expire in 0 days email landed in your inbox at dawn, open a terminal before you pour coffee. There is only one command to run before the site goes down.
sudo certbot renew --dry-runThis command simulates the renewal process without touching the live certificate. The error it prints is exactly why a real renewal would fail. This article maps those messages to causes and gives you copy-paste commands to fix them.
Note: if renewal succeeded but clients (curl, Java apps, etc.) still fail certificate trust, this is not the article you want—read the 'unable to get local issuer certificate' post instead. This article covers server-side certificate renewal; that one covers client-side CA chain verification. The symptoms look similar; the causes are completely different.
Step 1: Reproduce the failure with dry-run and read the logs
First, check the current certificate status and expiry date.
sudo certbot certificatesLook at Expiry Date and VALID: N days in the output. If expiry is imminent, run --dry-run immediately and inspect the logs on failure.
# Detailed log of the most recent attempt
sudo tail -n 50 /var/log/letsencrypt/letsencrypt.log
# For snap-based certbot, check timer logs with journalctl
sudo journalctl -u certbot --since "1 hour ago"Recent certbot often ships as a snap by default, so the binary path is frequently /snap/bin/certbot, and ECDSA is now the default certificate type. The log location (/var/log/letsencrypt/) is unchanged, so start there. Grab the one key line from the log and jump straight to the matching cause below.
Step 2: Cause-by-cause playbook (copy-paste commands)
Cause 1: Port 80 blocked (HTTP-01 challenge)
If you see this in the logs, it is a firewall/security group problem.
Timeout during connect (likely firewall problem)HTTP-01 challenge requires Let's Encrypt servers to reach your port 80 from the outside. If that is blocked, renewal always fails.
# Who is listening on port 80
sudo ss -tlnp | grep :80
# Allow through the OS firewall
sudo ufw allow 80/tcp
# On cloud (AWS/GCP/Oracle), also confirm security group / network ACL inbound 80 is openOn cloud instances especially, opening the OS firewall still times out if security-group inbound is closed. Check both.
Cause 2: nginx/apache plugin conflict
Here's how to choose a plugin.
| Situation | Recommended option |
|---|---|
| nginx running, auto config edits OK | --nginx |
| Leave the web server as-is; verify via files only | --webroot -w /var/www/html |
| No web server / one-off verification | --standalone |
--standalone binds port 80 itself, so it conflicts if nginx already owns port 80. In that case, use hooks to stop and start briefly.
sudo certbot certonly --standalone \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx" \
-d example.comCause 3: DNS-01 validation failure
Wildcard (*.example.com) certificates require DNS-01. It fails when the TXT record has not propagated.
# Confirm _acme-challenge TXT record has propagated
dig TXT _acme-challenge.example.com +shortIf you use a DNS plugin, credential file permissions are a common trap.
# Cloudflare example: credentials file must be mode 600
chmod 600 ~/.secrets/cloudflare.ini
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d "*.example.com" -d example.comCause 4: Rate limit exceeded
If you see this in the logs, you have hit the limit.
too many certificates already issuedLet's Encrypt allows 50 certificates per registered domain per week and 5 duplicate certificates per week. To avoid burning the quota with repeated failures, always test on staging first.
sudo certbot certonly --staging -d example.comStaging has a separate quota, so you can test freely. Once it works, drop --staging and run for real. If the limit is already hit, you typically wait up to a week for it to reset—so the real lesson is: don't burn quota on tests.
Cause 5: Expired because the timer/cron never ran
This is the most common "silent death" pattern: the command works, but auto-renewal never ran, so the cert expired.
# Check the systemd timer
systemctl list-timers | grep certbot
systemctl status certbot.timer
# If you use cron
crontab -lIf certbot.timer is inactive or missing from list-timers, auto-renewal has stopped.
sudo systemctl enable --now certbot.timerStep 3: Prevent recurrence — automate nginx reload with a deploy-hook
The trap I see most in production is this one: renewal succeeded, but the browser still says "expired." The cause is simple: nginx is still holding the old certificate in memory because it never reloaded.
Attach a deploy-hook so nginx reloads only on successful renewal.
sudo certbot renew --deploy-hook "systemctl reload nginx"For domains already issued, permanently add a line to /etc/letsencrypt/renewal/<domain>.conf.
# /etc/letsencrypt/renewal/example.com.conf
renew_hook = systemctl reload nginxWith short-lived ACME certificates (6-day lifetime) and OCSP stapling being retired, auto-renewal reliability matters even more going forward. The shorter the renewal cycle, the faster a missed hook becomes an outage—treat deploy-hook as mandatory, not optional.
Step 4: Emergency issuance on D-1 (expires tomorrow)
If auto-renewal is broken and the cert expires tomorrow, force a new issuance with minimal downtime.
sudo certbot certonly --standalone \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx" \
-d example.com --force-renewal--force-renewal issues a new certificate regardless of remaining validity. nginx downtime is usually a few seconds, so aim for a low-traffic window—or, if you can, issue with --webroot for zero downtime.
Conclusion: Copy-paste checklist
- Run
certbot renew --dry-runand capture the error message - Match keywords in
/var/log/letsencrypt/letsencrypt.log - timeout → port 80 / security group; "too many certificates" → rate limit / staging
- DNS-01 →
dig TXT _acme-challenge..., credential file mode 600 - Check auto-renewal with
systemctl list-timers | grep certbot - Automate nginx reload with a deploy-hook
One more time: if renewal clearly succeeded but clients still fail trust, that is not a server-side renewal problem—it is client-side CA chain verification. See the 'unable to get local issuer certificate' article.
FAQ
Q. dry-run succeeds but the real renewal fails. Why?
A. dry-run uses the staging server, so it is not subject to production rate limits. If only the real renewal fails, you most likely hit the production rate limit (too many certificates already issued). Check the logs and wait for the window to reset.
Q. Renewal succeeded but the browser still shows expired.
A. nginx/apache is still serving the old cert from memory. Manually run systemctl reload nginx, and register renew_hook = systemctl reload nginx to prevent it next time.
Q. Only one of several domains fails to renew.
A. That domain's DNS A record likely points at a different server, or the port-80 validation path is blocked. Check the failing domain individually with dig and curl -I http://<domain>/.well-known/acme-challenge/test.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.