Automatic SSL Renewal
This guide explains how to configure automatic renewal of Let's Encrypt SSL certificates.
Check Current Status
Installed Certificates
sudo certbot certificates
Example output:
Certificate Name: mydomain.com
Domains: mydomain.com www.mydomain.com
Expiry Date: 2024-03-15 10:30:00+00:00 (VALID: 45 days)
Certificate Path: /etc/letsencrypt/live/mydomain.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/mydomain.com/privkey.pem
Check Certificate Expiration
# Via certbot
sudo certbot certificates | grep -A 3 "Certificate Name"
# Via openssl (on a domain)
echo | openssl s_client -servername mydomain.com -connect mydomain.com:443 2>/dev/null | openssl x509 -noout -dates
# Via openssl (local file)
sudo openssl x509 -enddate -noout -in /etc/letsencrypt/live/mydomain.com/cert.pem
Automatic Renewal with Certbot
Check systemd Timer
Certbot automatically installs a timer:
# Check that the timer is active
sudo systemctl status certbot.timer
# See scheduled timers
sudo systemctl list-timers | grep certbot
Test Renewal
# Test without actually renewing
sudo certbot renew --dry-run
If the test succeeds, automatic renewal will work.
Renewal Configuration
The configuration file is located at /etc/letsencrypt/renewal/mydomain.com.conf:
[renewalparams]
authenticator = nginx
account = xxxxxxxxxxxxx
server = https://acme-v02.api.letsencrypt.org/directory
[webroot]
# or other options depending on your configuration
Renewal Hooks
Hooks allow executing commands before/after renewal.
Hook Structure
/etc/letsencrypt/renewal-hooks/
├── pre/ # Before renewal
├── deploy/ # After successful renewal
└── post/ # After renewal (success or failure)
Hook to Reload Nginx
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Hook to Reload Apache
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
#!/bin/bash
systemctl reload apache2
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
Notification Hook
sudo nano /etc/letsencrypt/renewal-hooks/deploy/notify.sh
#!/bin/bash
DOMAIN=$RENEWED_DOMAINS
EXPIRY=$(openssl x509 -enddate -noout -in "$RENEWED_LINEAGE/cert.pem" | cut -d= -f2)
# Email notification
echo "Certificate for $DOMAIN has been renewed. New expiration: $EXPIRY" | \
mail -s "SSL renewed: $DOMAIN" admin@yourdomain.com
# Discord notification (optional)
curl -H "Content-Type: application/json" \
-d "{\"content\": \"SSL certificate for **$DOMAIN** has been renewed. Expires: $EXPIRY\"}" \
"YOUR_DISCORD_WEBHOOK"
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/notify.sh
Renewal with Cron (alternative)
If the systemd timer doesn't work:
sudo crontab -e
# SSL renewal 2x per day
0 3,15 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"
Certificate Monitoring
Verification Script
sudo nano /usr/local/bin/check-ssl.sh
#!/bin/bash
ALERT_DAYS=14
EMAIL="admin@yourdomain.com"
for cert in /etc/letsencrypt/live/*/cert.pem; do
DOMAIN=$(basename $(dirname $cert))
EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt $ALERT_DAYS ]; then
echo "ALERT: $DOMAIN expires in $DAYS_LEFT days ($EXPIRY_DATE)" | \
mail -s "SSL ALERT: $DOMAIN" $EMAIL
fi
echo "$DOMAIN: $DAYS_LEFT days remaining"
done
sudo chmod +x /usr/local/bin/check-ssl.sh
Add to cron:
# Daily check at 9am
0 9 * * * /usr/local/bin/check-ssl.sh
Monitoring with curl
Check a remote certificate:
#!/bin/bash
# check-remote-ssl.sh
DOMAIN="$1"
ALERT_DAYS=14
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 ))
echo "$DOMAIN: $DAYS_LEFT days remaining (expires $EXPIRY)"
if [ $DAYS_LEFT -lt $ALERT_DAYS ]; then
exit 1 # To alert in a monitoring system
fi
Troubleshooting
Renewal Fails
HTTP Validation Error
# Check that the challenge is accessible
curl http://mydomain.com/.well-known/acme-challenge/test
# Check Nginx configuration
location /.well-known/acme-challenge/ {
root /var/www/html;
}
Port 80 Blocked
# Check that port 80 is open
sudo ufw status | grep 80
sudo ufw allow 80/tcp
Too Many Attempts (rate limit)
Let's Encrypt limits to 5 failures per hour. Wait before retrying.
# Use staging server for tests
sudo certbot certonly --staging -d mydomain.com
Force Renewal
# Renew a specific certificate
sudo certbot renew --cert-name mydomain.com --force-renewal
# Renew all certificates
sudo certbot renew --force-renewal
Check Logs
# Certbot logs
sudo cat /var/log/letsencrypt/letsencrypt.log
# Recent logs
sudo journalctl -u certbot -f
Wildcard Certificates
Wildcard certificates require DNS validation:
sudo certbot certonly --manual --preferred-challenges dns -d "*.mydomain.com" -d "mydomain.com"
Automation with DNS API
For Cloudflare:
sudo apt install python3-certbot-dns-cloudflare
Create credentials file:
sudo nano /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = YOUR_API_TOKEN
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
Obtain certificate:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.mydomain.com" \
-d "mydomain.com"
Best Practices
- Always test with
--dry-runbefore modifying configuration - Configure alerts to be notified before expiration
- Use deploy hooks rather than post for post-renewal actions
- Keep port 80 open even if you redirect to HTTPS
- Monitor logs regularly
Tip
Let's Encrypt certificates are valid for 90 days. Certbot automatically renews when less than 30 days remain.