/인프라/Fixing certbot Renewal Failures: Cause-by-Cause Troubleshooting for certificate expired
Infrastructurecertbot 갱신 실패lets encrypt 인증서 만료

Fixing certbot Renewal Failures: Cause-by-Cause Troubleshooting for certificate expired

Diagnose and fix certbot renew failures and certificate expired errors, starting with a dry-run. A practical copy-paste guide covering port 80 challenges, DNS-01, rate limits, and systemd timers that never ran.

Fixing certbot Renewal Failures: Cause-by-Cause Troubleshooting for certificate expired

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.

Bash
sudo certbot renew --dry-run

This 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.

Bash
sudo certbot certificates

Look at Expiry Date and VALID: N days in the output. If expiry is imminent, run --dry-run immediately and inspect the logs on failure.

Bash
# 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.

CODE
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.

Bash
# 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 open

On 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.

SituationRecommended 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.

Bash
sudo certbot certonly --standalone \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx" \
  -d example.com

Cause 3: DNS-01 validation failure

Wildcard (*.example.com) certificates require DNS-01. It fails when the TXT record has not propagated.

Bash
# Confirm _acme-challenge TXT record has propagated
dig TXT _acme-challenge.example.com +short

If you use a DNS plugin, credential file permissions are a common trap.

Bash
# 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.com

Cause 4: Rate limit exceeded

If you see this in the logs, you have hit the limit.

CODE
too many certificates already issued

Let'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.

Bash
sudo certbot certonly --staging -d example.com

Staging 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.

Bash
# Check the systemd timer
systemctl list-timers | grep certbot
systemctl status certbot.timer

# If you use cron
crontab -l

If certbot.timer is inactive or missing from list-timers, auto-renewal has stopped.

Bash
sudo systemctl enable --now certbot.timer

Step 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.

Bash
sudo certbot renew --deploy-hook "systemctl reload nginx"

For domains already issued, permanently add a line to /etc/letsencrypt/renewal/<domain>.conf.

INI
# /etc/letsencrypt/renewal/example.com.conf
renew_hook = systemctl reload nginx

With 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.

Bash
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

  1. Run certbot renew --dry-run and capture the error message
  2. Match keywords in /var/log/letsencrypt/letsencrypt.log
  3. timeout → port 80 / security group; "too many certificates" → rate limit / staging
  4. DNS-01 → dig TXT _acme-challenge..., credential file mode 600
  5. Check auto-renewal with systemctl list-timers | grep certbot
  6. 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.