Introduction

Automated certificate renewal should keep your SSL certificates valid without manual intervention. When renewal stops working, certificates eventually expire and break your site. Common causes include cron job issues, service failures, port availability problems, or changed server configurations. Diagnosing and fixing the renewal process prevents expiration emergencies.

Symptoms

  • Certificate expires despite auto-renewal setup
  • Certbot renew fails silently or with errors
  • Cron job or timer not running
  • Renewal logs show failures
  • Port 80 blocked during renewal attempts
  • Rate limits hit from repeated failed attempts
  • Certificate shows old dates after renewal attempt

Common Causes

  • Cron job disabled or removed
  • systemd timer not active
  • Port 80 blocked (firewall, nginx/apache)
  • Webroot path changed, certbot can't write challenge
  • Certificate permissions or path changed
  • DNS changed, challenges failing
  • certbot or acme.sh service stopped
  • Hook scripts failing after renewal
  • Renewal configuration corrupted

Step-by-Step Fix

Step 1: Check Current Renewal Status

```bash # Check certificate expiration certbot certificates

# Check when renewal will be attempted certbot renew --dry-run

# Check systemd timers systemctl list-timers | grep certbot

# Check cron jobs crontab -l | grep certbot cat /etc/cron.d/certbot cat /etc/crontab | grep certbot ```

Step 2: Check Renewal Logs

```bash # Certbot logs location cat /var/log/letsencrypt/letsencrypt.log

# Recent renewal attempts grep -i "renew" /var/log/letsencrypt/letsencrypt.log | tail -20

# Check for specific errors grep -i "error|fail" /var/log/letsencrypt/letsencrypt.log | tail -20

# Systemd journal for timer journalctl -u certbot.timer --since "30 days ago" journalctl -u certbot.service --since "30 days ago" ```

Step 3: Test Manual Renewal

```bash # Dry run first certbot renew --dry-run

# If dry run fails, check the error: # - Port 80 blocked: check firewall, web server # - Webroot wrong: check nginx/apache config # - DNS wrong: check DNS records

# Attempt actual renewal certbot renew

# Force renewal for testing certbot renew --force-renewal ```

Step 4: Fix Cron/Timer Issues

systemd timer fix:

```bash # Check timer status systemctl status certbot.timer

# Enable and start timer systemctl enable certbot.timer systemctl start certbot.timer

# Check timer schedule systemctl list-timers certbot.timer

# If timer missing, recreate: systemctl enable certbot-renew.timer ```

Cron job fix:

```bash # Check if certbot cron exists ls /etc/cron.d/certbot

# If missing, recreate: cat > /etc/cron.d/certbot << EOF 0 0,12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q EOF

# Or simpler: echo "0 3 * * * root certbot renew --quiet" > /etc/cron.d/certbot

# Ensure cron daemon running systemctl status cron systemctl enable cron systemctl start cron ```

Step 5: Fix Port 80 Access

```bash # Renewal needs port 80 for HTTP-01 challenge # Check firewall iptables -L -n | grep 80 ufw status | grep 80 firewall-cmd --list-ports

# Open port 80 ufw allow 80/tcp iptables -I INPUT -p tcp --dport 80 -j ACCEPT firewall-cmd --add-port=80/tcp --permanent

# Check web server listening on port 80 netstat -tlnp | grep :80 ss -tlnp | grep :80

# If nothing listening, check nginx/apache systemctl status nginx apache2 ```

Step 6: Fix Webroot Configuration

```bash # Check certbot renewal config cat /etc/letsencrypt/renewal/example.com.conf | grep webroot

# Check if webroot path exists ls -la /var/www/html/.well-known/acme-challenge/

# If path changed, update renewal config # Or use standalone mode instead certbot renew --preferred-challenges standalone

# Stop web server before standalone renewal systemctl stop nginx certbot renew systemctl start nginx ```

Step 7: Fix Hook Scripts

```bash # Check if hooks configured cat /etc/letsencrypt/renewal/example.com.conf | grep -i hook

# Test pre-hook cat /etc/letsencrypt/renewal/example.com.conf | grep pre_hook # If pre_hook stops nginx, verify it works

# Test post-hook cat /etc/letsencrypt/renewal/example.com.conf | grep post_hook # If post_hook reloads nginx, verify nginx -t works first

# Fix broken hook # Edit renewal config: # pre_hook = systemctl stop nginx # post_hook = systemctl start nginx # Or remove hooks and handle manually ```

Step 8: Fix acme.sh Renewal Issues

```bash # Check acme.sh status acme.sh --list

# Check cron job crontab -l | grep acme

# Force renewal acme.sh --renew -d example.com --force

# Check acme.sh cron acme.sh --install-cronjob

# Check logs cat ~/.acme.sh/acme.sh.log ```

Step 9: Verify Renewal Works

```bash # After fixes, test renewal certbot renew --dry-run

# Check certificate dates after successful test certbot certificates

# Simulate timer run systemctl start certbot.service journalctl -u certbot.service -n 50

# Check systemd timer scheduled systemctl list-timers | grep certbot ```

Step 10: Set Up Monitoring

```bash # Add certificate expiration monitoring # Simple check script: cat > /usr/local/bin/check-cert.sh << 'EOF' #!/bin/bash DOMAIN="example.com" EXPIRY=$(echo | openssl s_client -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 30 ]; then echo "Certificate for $DOMAIN expires in $DAYS_LEFT days" exit 1 fi EOF chmod +x /usr/local/bin/check-cert.sh

# Add to cron for daily check echo "0 6 * * * root /usr/local/bin/check-cert.sh" > /etc/cron.d/cert-check ```

Common Pitfalls

  • Cron job removed during system update
  • systemd timer disabled accidentally
  • Port 80 blocked by new firewall rules
  • Webroot path changed without updating certbot config
  • Pre/post hooks failing silently
  • Rate limit exhaustion from repeated failed attempts
  • Certificate renewed but server not reloaded

Best Practices

  • Test renewal with --dry-run monthly
  • Monitor certificate expiration dates
  • Keep renewal logs accessible for troubleshooting
  • Ensure port 80 always available for HTTP-01
  • Document renewal configuration and hooks
  • Set up alerts for certificate expiration
  • Verify post-hook reloads web server
  • Test entire renewal process regularly
  • SSL Certificate Expired
  • Let's Encrypt Rate Limit Exceeded
  • Let's Encrypt Challenge Failed
  • SSL Certificate Chain Incomplete