BlogDevOps
DevOps

SSL Certificate Auto-Renewal Failing Silently with Certbot and Nginx

Your SSL certificates expired and your site went down because Certbot renewal failed silently. Learn every reason this happens — DNS propagation, hook scripts, systemd timer issues, and port conflicts — with tested solutions.

M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

February 6, 2026
15 min read

You set up Let's Encrypt certificates with Certbot months ago. Everything worked perfectly. Then one morning, your users start reporting that your site shows a security warning — your certificate expired three days ago. You check the server and discover that Certbot's automatic renewal has been failing silently for weeks. No alerts, no emails, no indication that anything was wrong until users could not access your site.

This is one of the most common production incidents in the industry, and it happens because certificate renewal involves multiple moving parts that can each fail independently. Certbot needs to prove you control the domain, obtain the new certificate, install it, and reload the web server — and any step can break without obvious symptoms.

How Certbot Auto-Renewal Works

When you install Certbot, it creates a systemd timer (on modern Linux systems) or a cron job that runs certbot renew twice per day. This command checks every certificate managed by Certbot, and if any certificate expires within 30 days, it attempts to renew it. Let's Encrypt certificates are valid for 90 days, so this gives a 60-day window for renewal to succeed.

Check the timer status to see if it is even running:

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

# Check cron job
sudo crontab -l
sudo cat /etc/cron.d/certbot

If you see "inactive (dead)" for the timer, that is your problem — the timer was never enabled or was disabled during a system update. Enable and start it with:

sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

Reason 1: Port 80 Is Not Accessible

The most common Certbot setup uses the HTTP-01 challenge, which requires Let's Encrypt's servers to connect to your server on port 80. Certbot places a challenge file at http://yourdomain.com/.well-known/acme-challenge/random-token, and Let's Encrypt's servers verify they can access it.

This fails when: port 80 is blocked by a firewall (common after security hardening that blocks all ports except 443); another process is using port 80 (Apache, a Docker container, or another Nginx instance); or the Nginx configuration does not serve the challenge directory correctly.

Ensure your Nginx configuration includes a location block for ACME challenges even in the HTTPS server block:

server {
    listen 80;
    server_name yourdomain.com;

    # ACME challenge directory — essential for certificate renewal
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        allow all;
    }

    # Redirect everything else to HTTPS
    location / {
        return 301 https://$server_name$request_uri;
    }
}

Test the challenge path manually:

# Create a test file
echo "test" | sudo tee /var/www/certbot/.well-known/acme-challenge/test-file

# Try to access it
curl -v http://yourdomain.com/.well-known/acme-challenge/test-file

# Clean up
sudo rm /var/www/certbot/.well-known/acme-challenge/test-file

If this curl command fails, the renewal will fail the same way.

Reason 2: DNS Configuration Changed

If you switched DNS providers, moved to Cloudflare, or changed your A record, the domain might not resolve to your server anymore — or it might resolve to a Cloudflare proxy IP that does not pass through HTTP-01 challenges correctly.

# Check what IP your domain resolves to
dig +short yourdomain.com A
dig +short www.yourdomain.com A

# Verify it matches your server
curl ifconfig.me

If you use Cloudflare with the proxy enabled (orange cloud), HTTP-01 challenges can still work, but you need to ensure Cloudflare is not blocking the challenge request. Alternatively, switch to DNS-01 challenges which do not require port 80 access at all:

# Install the Cloudflare DNS plugin
sudo apt install python3-certbot-dns-cloudflare

# Create credentials file
sudo cat > /etc/letsencrypt/cloudflare.ini <<EOF
dns_cloudflare_api_token = your-cloudflare-api-token
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

# Obtain certificate using DNS challenge
sudo certbot certonly --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
    -d yourdomain.com -d "*.yourdomain.com"

DNS-01 challenges are more reliable than HTTP-01 because they do not depend on port 80 accessibility, web server configuration, or network routing. They also support wildcard certificates.

Reason 3: The Reload Hook Is Missing or Broken

Certbot obtains the new certificate files but does not automatically tell Nginx to use them. Nginx reads certificate files at startup or reload — it does not monitor the filesystem for changes. Without a post-renewal hook that reloads Nginx, the old (expired) certificate remains in memory even though the new certificate is on disk.

# Create or verify the renewal hook
sudo cat /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

If this file does not exist, create it:

sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/bash
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

The nginx -t before the reload is critical — if the Nginx configuration is invalid for any reason, the reload would fail and Nginx would stop serving entirely. The test-then-reload pattern prevents this catastrophic failure.

Alternatively, set the hook in the renewal configuration file for each domain:

# /etc/letsencrypt/renewal/yourdomain.com.conf
[renewalparams]
# ... other parameters ...

[post-hook]
post_hook = nginx -t && systemctl reload nginx

Reason 4: Rate Limits

Let's Encrypt enforces rate limits to prevent abuse. If you hit these limits during testing or due to repeated failed renewals, subsequent renewal attempts will be rejected for a period. The most relevant limits are: 5 duplicate certificates per week, 50 certificates per registered domain per week, and 5 failed validation attempts per account per hostname per hour.

Check your rate limit status by examining the Certbot logs:

sudo cat /var/log/letsencrypt/letsencrypt.log | grep -i "rate limit"

If you are rate limited, you can only wait for the limit to reset. To avoid hitting limits during testing, always use the --staging flag:

sudo certbot certonly --staging -d yourdomain.com

Reason 5: Permission Issues

Certbot needs write access to /etc/letsencrypt/ and its subdirectories. If permissions were changed (perhaps during a security hardening exercise), renewal silently fails. Additionally, the renewal hook script must be executable.

# Verify permissions
sudo ls -la /etc/letsencrypt/
sudo ls -la /etc/letsencrypt/renewal/
sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/

# Fix permissions if needed
sudo chown -R root:root /etc/letsencrypt/
sudo chmod -R 755 /etc/letsencrypt/renewal-hooks/

Testing and Monitoring Renewal

Never assume renewal works — test it regularly. Certbot provides a dry-run mode that tests the entire renewal process without actually obtaining a certificate:

sudo certbot renew --dry-run

If this succeeds, the real renewal will succeed. If it fails, you see exactly which step failed and why. Run this after every configuration change.

Set up monitoring that alerts you when a certificate is approaching expiration. A simple script that runs weekly:

#!/bin/bash
DOMAIN="yourdomain.com"
EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_LEFT" -lt 14 ]; then
    echo "WARNING: SSL certificate for $DOMAIN expires in $DAYS_LEFT days!" | mail -s "SSL Certificate Expiring" admin@yourdomain.com
fi

External monitoring services like UptimeRobot, Pingdom, or StatusCake can also monitor SSL certificate expiry and alert you via email, SMS, or Slack.

ZeonEdge provides SSL configuration, certificate management, and infrastructure monitoring services. Learn more about our security services.

M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.