Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
The Error
Running certbot renew or certbot renew --dry-run fails with one of these errors:
Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf produced an unexpected error: Problem binding to port 80: Could not bind to IPv4 or IPv6.Or the HTTP-01 challenge fails:
Challenge failed for domain example.com
http-01 challenge for example.com
Cleaning up challenges
IMPORTANT NOTES:
- The following errors were reported by the server:
Domain: example.com
Type: connection
Detail: Timeout during connect (likely firewall problem)Or DNS validation fails:
Domain: example.com
Type: dns
Detail: DNS problem: SERVFAIL looking up CAA for example.comOr the certificate is near expiry but auto-renewal didn’t run:
Your certificate and chain have been saved at /etc/letsencrypt/live/example.com/fullchain.pem
Expiry date: 2026-04-01 (EXPIRED or about to expire)Why This Happens
Let’s Encrypt uses ACME challenges to verify domain ownership. Every renewal repeats the original validation: Certbot proves you still control the domain by serving a file (HTTP-01), a TLS extension (TLS-ALPN-01), or a TXT record (DNS-01). Anything that breaks that proof — a closed port, a redirect on the wrong location block, a stale CAA record — makes the renewal fail even when the original issuance worked months earlier.
Renewals also depend on automation that runs while you aren’t looking. Certbot expects a systemd timer or cron job to fire twice a day, and most distributions install one when you run apt install certbot. If the timer is masked, the package was migrated to snap without removing the apt unit, or the server was restored from a snapshot with the timer disabled, the certificate ages silently until clients see expired certificate warnings.
The most common failure causes:
- Port 80 blocked — the HTTP-01 challenge requires port 80 to be open and reachable from the internet. Firewalls, security groups, or nginx already binding port 80 block the standalone authenticator.
- Nginx webroot misconfiguration — the
/.well-known/acme-challenge/path is not served by nginx, blocked by areturn 301redirect, or points to the wrong directory. - DNS not propagated — the DNS-01 challenge or CAA record lookup fails because DNS changes haven’t propagated, or the domain has a CAA record that excludes Let’s Encrypt.
- Certbot timer not running — the systemd timer or cron job that triggers
certbot renewis disabled or misconfigured, so the certificate expires without being renewed. - Rate limits hit — Let’s Encrypt enforces rate limits (5 duplicate certificates per week). Testing with production causes failures; use
--dry-runand the staging environment. - Certbot version outdated — old Certbot versions use deprecated APIs. Let’s Encrypt eventually drops support, causing all renewals to fail.
Version History That Changes the Failure Mode
Let’s Encrypt and Certbot have a long migration history, and an older deployment that “just worked” for years often breaks on a specific date in that timeline rather than on the day you noticed the failure.
DST Root CA X3 expiry (September 30, 2021). For years, Let’s Encrypt chains terminated at the IdenTrust DST Root CA X3 root for compatibility with old Android devices. That root expired on 30 September 2021. Clients that hadn’t received an update — older OpenSSL on CentOS 7, embedded Java keystores, Node.js bundled CA stores from 2019 — suddenly stopped trusting valid certificates with certificate has expired errors. Let’s Encrypt now defaults to its own ISRG Root X1, and you only fall back to the cross-signed chain if you ask for --preferred-chain "ISRG Root X1" and your CA bundle is current.
ACMEv1 shutdown (June 2021). Let’s Encrypt deprecated ACMEv1 and stopped accepting new account registrations against it in November 2019, then disabled renewals on existing v1 accounts in June 2021. Renewal configs that still reference acme-v01.api.letsencrypt.org/directory cannot renew at all. The fix is to update each /etc/letsencrypt/renewal/*.conf to the acme-v02 directory and re-register the account.
Certbot 1.x to 2.0 (November 2022). Certbot 2.0 dropped Python 2 support, removed several long-deprecated plugins (Apache plugin internals were rewritten), and changed how the snap classic confinement interacts with deploy hooks. If you upgrade from a 0.x or 1.x install — common on Ubuntu 18.04 boxes that have lingered — your hook scripts may stop firing because the snap version reads from /etc/letsencrypt/renewal-hooks/ but ignores hooks in older locations like /etc/letsencrypt/renewal/.
HTTP-01 vs DNS-01 behavior differences. Wildcard certificates require DNS-01; they were never possible with HTTP-01. Certbot only added the --preferred-challenges dns flag and the certbot-dns-* plugin family in the 0.22–0.31 timeframe. Older deploys that used acme.sh or third-party scripts often relied on manual DNS edits; modern Certbot expects an API token in cloudflare.ini, route53.ini, or similar.
Rate limit policy changes. The rolling-week duplicate certificate limit moved from 5 to 7 to 5 again across the 2017–2020 window, and Let’s Encrypt introduced separate limits for new orders, failed validations, and accounts per IP. A renewal that succeeded historically can hit the “too many failed authorizations” limit after several broken attempts in the same week.
nginx-ingress / cert-manager integration. If you renew certificates through cert-manager on Kubernetes rather than certbot renew directly, the failure shape is different: cert-manager creates Order and Challenge CRDs whose events surface the real cause (presented challenge but did not get a response, solver not configured for HTTP01). The fix is the same — port 80 reachable, DNS pointing at the ingress — but you debug it with kubectl describe challenge instead of journalctl.
Fix 1: Test Before Fixing
Always run a dry run first to diagnose without hitting rate limits:
# Dry run — tests the full renewal process without issuing a certificate
certbot renew --dry-run
# Dry run for a specific domain
certbot renew --dry-run -d example.com
# Verbose output to see exactly where it fails
certbot renew --dry-run --verboseCheck the current certificate status:
# List all certificates and their expiry dates
certbot certificates
# Output:
# Found the following certs:
# Certificate Name: example.com
# Domains: example.com www.example.com
# Expiry Date: 2026-06-15 09:00:00+00:00 (VALID: 89 days)
# Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pemFix 2: Fix Port 80 Blocked (HTTP-01 Challenge)
The HTTP-01 challenge requires port 80 to be accessible from the internet. Check what’s blocking it:
# Check if something is listening on port 80
sudo ss -tlnp | grep :80
# Check if nginx is running on port 80
sudo systemctl status nginx
# Test port 80 from outside your server (run from another machine or use a web tool)
curl -I http://example.com/.well-known/acme-challenge/testIf nginx is running, use the webroot plugin instead of standalone:
# Webroot plugin — lets nginx keep running while renewing
certbot renew --webroot -w /var/www/html
# Or renew with webroot for a specific cert
certbot certonly --webroot -w /var/www/certbot -d example.com -d www.example.comIf using standalone and nginx blocks port 80 — stop nginx during renewal:
# Stop nginx, renew, restart nginx
sudo systemctl stop nginx
sudo certbot renew
sudo systemctl start nginxAutomate stop/start with deploy hooks:
# /etc/letsencrypt/renewal-hooks/pre/stop-nginx.sh
#!/bin/bash
systemctl stop nginx# /etc/letsencrypt/renewal-hooks/post/start-nginx.sh
#!/bin/bash
systemctl start nginxchmod +x /etc/letsencrypt/renewal-hooks/pre/stop-nginx.sh
chmod +x /etc/letsencrypt/renewal-hooks/post/start-nginx.shCheck firewall rules:
# UFW — allow port 80 and 443
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
# iptables — check rules
sudo iptables -L -n | grep -E "80|443"
# AWS EC2 — check security group allows inbound TCP port 80
# GCP — check firewall rules for port 80
# Azure — check Network Security GroupFix 3: Fix Nginx Webroot Configuration
If using the webroot authenticator, nginx must serve the /.well-known/acme-challenge/ path correctly:
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com www.example.com;
# Required for Certbot webroot challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot; # Must match --webroot-path
allow all;
}
# Redirect everything else to HTTPS — but keep the challenge path accessible over HTTP
location / {
return 301 https://$host$request_uri;
}
}Common Mistake: Putting
return 301 https://...before the/.well-known/acme-challenge/block redirects the ACME challenge to HTTPS, causing the HTTP-01 challenge to fail. Always place the challenge location block before the redirect.
Verify the challenge path is reachable:
# Create the webroot directory
sudo mkdir -p /var/www/certbot
# Test that nginx serves files from it
echo "test" | sudo tee /var/www/certbot/.well-known/acme-challenge/test
curl http://example.com/.well-known/acme-challenge/test
# Should return: test
# Clean up
sudo rm /var/www/certbot/.well-known/acme-challenge/testCheck the renewal configuration file to verify webroot path:
cat /etc/letsencrypt/renewal/example.com.conf
# Look for:
# authenticator = webroot
# webroot_path = /var/www/certbotIf the path is wrong, edit it or re-run certbot certonly --webroot -w /correct/path -d example.com.
Fix 4: Fix DNS Issues (CAA Records and DNS-01 Challenge)
Check for CAA records that block Let’s Encrypt:
# Check CAA records
dig CAA example.com
# Expected output if you have a CAA record allowing Let's Encrypt:
# example.com. 300 IN CAA 0 issue "letsencrypt.org"If you have a CAA record that doesn’t include letsencrypt.org, add one:
# DNS zone — add CAA record
example.com. 300 IN CAA 0 issue "letsencrypt.org"
example.com. 300 IN CAA 0 issuewild "letsencrypt.org"Check DNS propagation:
# Check if your domain resolves to the right IP
dig A example.com
nslookup example.com
# The IP must match the server where Certbot is running
curl -4 ifconfig.me # Your server's public IPFor DNS-01 challenge (wildcard certs), use a plugin that supports your DNS provider:
# Install DNS plugin for your provider
pip install certbot-dns-cloudflare
pip install certbot-dns-route53
# Cloudflare example
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d example.com \
-d "*.example.com"# ~/.secrets/certbot/cloudflare.ini
dns_cloudflare_api_token = your_cloudflare_api_tokenchmod 600 ~/.secrets/certbot/cloudflare.iniFix 5: Fix Systemd Timer Not Running
Certbot installed via system packages (apt, dnf) uses a systemd timer. Check if it’s running:
# Check the timer status
sudo systemctl status certbot.timer
# If inactive or failed, enable and start it
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
# List all timers — verify certbot is scheduled
sudo systemctl list-timers --all | grep certbotIf the timer doesn’t exist, check for a cron job:
# Check cron jobs
sudo crontab -l
sudo cat /etc/cron.d/certbot
# Manual cron setup (if timer doesn't exist)
sudo crontab -e
# Add:
0 0,12 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"Test the renewal manually:
# Simulate what the timer would do
sudo certbot renew --quiet
# Check the systemd journal for renewal logs
sudo journalctl -u certbot.service --since "7 days ago"Fix 6: Fix Certificate After Renewal (Nginx Reload)
Renewing the certificate doesn’t automatically reload nginx. The old certificate stays in memory until nginx restarts:
# After manual renewal — reload nginx to use the new certificate
sudo nginx -t && sudo systemctl reload nginx
# Verify the new certificate is being served
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
# notAfter should show the new expiry dateUse a deploy hook to reload nginx automatically after every renewal:
# Create a deploy hook
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh#!/bin/bash
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
nginx -t && systemctl reload nginxsudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shNow every successful renewal automatically reloads nginx.
Fix 7: Fix Rate Limit Issues
Let’s Encrypt allows 5 duplicate certificates per domain per week. If you hit this:
# Always use --dry-run for testing
certbot renew --dry-run
# Use the staging environment for development testing
certbot certonly --staging -d example.com -d www.example.com --webroot -w /var/www/certbotCheck your current rate limit status:
- Visit
https://crt.sh/?q=example.comto see all certificates issued for your domain. - If you see many recent certificates, wait until the rolling 7-day window passes.
If you’ve hit rate limits and need a certificate urgently:
# Use the staging environment — unlimited issuance, but not trusted by browsers
certbot certonly --staging \
--webroot -w /var/www/certbot \
-d example.com -d www.example.com
# Once rate limits reset, issue a production certificate
certbot certonly \
--webroot -w /var/www/certbot \
-d example.com -d www.example.comFix 8: Update Certbot
Outdated Certbot versions fail when Let’s Encrypt deprecates old API endpoints:
# Check current version
certbot --version
# Ubuntu/Debian — update via snap (recommended)
sudo snap install --classic certbot
sudo snap refresh certbot
# Ubuntu/Debian — update via apt
sudo apt update && sudo apt upgrade certbot python3-certbot-nginx
# CentOS/RHEL — update via dnf/yum
sudo dnf update certbot
# pip installation — update
pip install --upgrade certbot certbot-nginxMigrate from apt to snap (recommended for Ubuntu):
# Remove old apt version
sudo apt remove certbot
# Install via snap
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# Verify
certbot --versionStill Not Working?
Check the Certbot log for detailed errors:
sudo tail -100 /var/log/letsencrypt/letsencrypt.logVerify the renewal config file is correct:
sudo cat /etc/letsencrypt/renewal/example.com.conf
# Check: authenticator, webroot_path, server (should be acme-v02, not acme-v01)If server in the config still points to acme-v01.api.letsencrypt.org:
# Update all renewal configs to the v02 endpoint
sudo sed -i 's|acme-v01.api.letsencrypt.org/directory|acme-v02.api.letsencrypt.org/directory|g' /etc/letsencrypt/renewal/*.confCheck if Let’s Encrypt itself has an outage: Visit https://letsencrypt.status.io/ — if there’s a known incident, wait and retry.
Delete and reissue if the cert is corrupted:
# Backup first
sudo cp -r /etc/letsencrypt /etc/letsencrypt.bak
# Delete the old cert and reissue
sudo certbot delete --cert-name example.com
sudo certbot certonly --webroot -w /var/www/certbot -d example.com -d www.example.com
sudo systemctl reload nginxCheck the account TOS acceptance. Let’s Encrypt occasionally rotates its subscriber agreement. If the account JSON in /etc/letsencrypt/accounts/ references a TOS URL that no longer matches the current one, ACMEv2 returns urn:ietf:params:acme:error:userActionRequired. Re-register with certbot register --agree-tos --email [email protected] --force-interactive to refresh the agreement.
Check IPv6 reachability. Let’s Encrypt validates over IPv6 first when the domain has an AAAA record. If your nginx listens only on IPv4 but the DNS publishes both A and AAAA, the validator may try IPv6 and fail before falling back. Either remove the AAAA record, add listen [::]:80; to the server block, or use --preferred-challenges http and verify both stacks accept the challenge.
Check the snap vs apt conflict. On Ubuntu, having both apt-installed certbot and the snap package present causes two certbot.timer units to compete. Run which -a certbot and systemctl list-units 'certbot*'. Remove whichever you aren’t using and rerun systemctl daemon-reload.
For related SSL and nginx issues, see Fix: Nginx SSL Handshake Failed, Fix: Nginx 502 Bad Gateway, Fix: Nginx 403 Forbidden, and Fix: Nginx Location Block Not Matching.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Nginx location Block Not Matching (Wrong Route Served)
How to fix Nginx location blocks not matching — caused by prefix vs regex priority, trailing slash issues, root vs alias confusion, and try_files misconfiguration.
Fix: Nginx SSL: error:0A00006C:SSL routines::bad key / SSL handshake failed
How to fix Nginx SSL handshake failed and certificate errors caused by mismatched keys, wrong certificate chain, expired certs, TLS version issues, and permission problems.
Fix: nginx Upstream Load Balancing Not Working — All Traffic Hitting One Server
How to fix nginx load balancing issues — upstream block configuration, health checks, least_conn vs round-robin, sticky sessions, upstream timeouts, and SSL termination.
Fix: Docker Container Keeps Restarting
How to fix a Docker container that keeps restarting — reading exit codes, debugging CrashLoopBackOff, fixing entrypoint errors, missing env vars, out-of-memory kills, and restart policy misconfiguration.