Skip to main content

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

  1. Always test with --dry-run before modifying configuration
  2. Configure alerts to be notified before expiration
  3. Use deploy hooks rather than post for post-renewal actions
  4. Keep port 80 open even if you redirect to HTTPS
  5. Monitor logs regularly
Tip

Let's Encrypt certificates are valid for 90 days. Certbot automatically renews when less than 30 days remain.