SSL Certificate Renewal Runbook
Quick Reference:
- Duration: 5-15 minutes
- Impact: Minimal (usually zero downtime)
- Requires: Sudo access to load balancer or cert-manager admin
- Severity: HIGH (expired certs = full outage)
Prerequisites
What You Need
- SSH access to certificate servers or kubectl access to cluster
- Let’s Encrypt API credentials (if manual renewal)
- Backup certificate location documented
- 30+ days lead time before expiry (not 5 minutes!)
Check Current Status
# View certificate expiry
openssl s_client -connect example.com:443 -showcerts | grep -A5 "Verify return code"
# Or via Kubernetes
kubectl get certificate -n ingress-nginx
kubectl describe certificate prod-cert -n ingress-nginx
# Check expiry date specifically
openssl x509 -in /etc/ssl/certs/server.crt -noout -enddate
Automatic Renewal (Preferred)
Using cert-manager (Kubernetes)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: prod-cert
namespace: ingress-nginx
spec:
secretName: prod-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- "*.example.com"
Cert-manager automatically:
- ✅ Renews 30 days before expiry
- ✅ Handles DNS validation
- ✅ Stores in Kubernetes Secret
- ✅ Reloads ingress automatically
Check renewal:
kubectl get events -n ingress-nginx | grep Certificate
kubectl logs -n cert-manager deploy/cert-manager | grep prod-cert
Using Certbot (Traditional)
# Automatic renewal via cron (runs daily)
0 2 * * * /usr/bin/certbot renew --quiet --no-eff-email
# Manual renewal when needed
certbot renew --force-renewal
# Renew specific domain
certbot certonly --standalone -d example.com -d "*.example.com"
After renewal:
# Reload web server
sudo systemctl reload nginx
# OR
sudo systemctl reload apache2
# Verify new certificate
openssl s_client -connect example.com:443 | grep -A2 "subject="
Manual Renewal Process
If Automatic Renewal Failed
Step 1: Stop web server (if using standalone validation)
sudo systemctl stop nginx
Step 2: Request new certificate
sudo certbot certonly \
--standalone \
-d example.com \
-d "*.example.com" \
--agree-tos \
-m [email protected]
Step 3: Verify new certificate location
ls -lah /etc/letsencrypt/live/example.com/
# Should show: cert.pem, chain.pem, fullchain.pem, privkey.pem
Step 4: Update web server config (if using non-LE certs)
# Update nginx/apache config to point to new cert path
sudo nano /etc/nginx/nginx.conf
# Point to: /etc/letsencrypt/live/example.com/fullchain.pem
# Point to: /etc/letsencrypt/live/example.com/privkey.pem
Step 5: Test and reload
# Test configuration syntax
sudo nginx -t
# Reload gracefully
sudo systemctl reload nginx
# Verify it works
curl -I https://example.com
Troubleshooting
Issue: Rate Limit Exceeded
Symptoms: Renewal fails with “too many certificates already issued”
Solution:
# Check how many certs issued
whois domain.com | grep -i "created"
# Wait before retrying (Let's Encrypt limits):
# - 50 certs per domain per week
# - 5 duplicate certs per week
# Use staging environment for testing
certbot certonly --staging -d example.com
# After 7 days, try again
Issue: DNS Validation Failed
Symptoms: ACME challenge fails, “DNS problem”
Solution:
# Check DNS propagation
nslookup _acme-challenge.example.com
# Wait for DNS to propagate (up to 48 hours)
# Then retry
# Use manual DNS validation if needed
certbot certonly --manual -d example.com
# Follow prompts to create DNS TXT records
Issue: Certificate Not Updating
Symptoms: Old certificate still showing after renewal
Solution:
# Clear web browser cache
# (Certificates often cached for HSTS preload)
# Check if nginx/apache reloaded
ps aux | grep -i nginx | grep -i reload
# If not running, restart:
sudo systemctl restart nginx
# Check certificate timestamp
openssl s_client -connect example.com:443 -showcerts | grep notBefore
# Force immediate renewal
sudo certbot renew --force-renewal
# Check for certbot errors
sudo certbot renew -v
tail -f /var/log/letsencrypt/letsencrypt.log
Issue: Multiple Domains/Subdomains
Symptoms: Need to renew certificate with additional domains
Solution:
# List current certificates
sudo certbot certificates
# Expand existing certificate
sudo certbot certonly \
--expand \
-d example.com \
-d "*.example.com" \
-d "api.example.com"
# Or request new with all domains
sudo certbot certonly \
--standalone \
-d example.com \
-d "*.example.com" \
-d "api.example.com" \
-d "admin.example.com"
Pre-Expiry Checklist (30 days before)
Verify automatic renewal is working
certbot renew --dry-run
Check DNS settings are correct
nslookup example.com dig example.com
Ensure backup certificate is up-to-date
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem /backups/
Document load balancer certificate paths
grep -r "ssl_certificate" /etc/nginx/
Enable monitoring/alerting for certificate expiry
# Prometheus query ssl_certificate_not_after_seconds - time() < 2592000 # 30 days
Post-Renewal Verification
Test the certificate:
# Check certificate is valid
openssl s_client -connect example.com:443 -showcerts
# Should show:
# - subject= /CN=example.com
# - issuer= /C=US/O=Let's Encrypt/CN=R3
# - Verify return code: 0 (ok)
# Check expiry date
openssl s_client -connect example.com:443 \
| openssl x509 -noout -enddate
# Should be ~90 days from today
Monitor after renewal:
- ✅ Watch application logs for SSL errors
- ✅ Monitor SSL certificate metrics
- ✅ Check that HSTS headers are present
- ✅ Verify no browser warnings
Common Mistakes to Avoid
❌ Don’t
- Wait until certificate expires (0 day deadline = panic)
- Renew manually every 90 days (set up automation!)
- Use self-signed certs in production
- Forget to reload web server after renewal
- Store private keys in version control
- Mix certificate renewal timezones
✅ Do
- Set renewal reminder for 30 days before expiry
- Use cert-manager or certbot with automation
- Store backup certificates in secure location
- Test renewal in staging first
- Monitor certificate expiry with alerts
- Document certificate locations and procedures
Monitoring & Alerts
Prometheus Alert Rule
- alert: CertificateExpirySoon
expr: ssl_certificate_not_after_seconds - time() < 2592000
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate expires in {{ $value | humanizeDuration }}"
- alert: CertificateExpiryImmediate
expr: ssl_certificate_not_after_seconds - time() < 86400
labels:
severity: critical
annotations:
summary: "Certificate expires in less than 24 hours!"
Kubernetes Cert-Manager Alerts
# Check cert-manager reconciliation
kubectl get events -n cert-manager | grep Certificate
# Check certificate status
kubectl get cert -n ingress-nginx -o wide
# Watch for warning events
kubectl get events -A --sort-by='.lastTimestamp' | grep Warning
References
Last Updated: 2025-08-15 Owner: Platform Team Reviewed By: Security Team Next Review: 2025-12-15