Certificate Maintenance Skill
Operations for monitoring, maintaining, and managing SSL/TLS certificates in the Caddy reverse proxy with Let's Encrypt.
Quick Start
Quick certificate status check:
# Check expiry for single domain
echo | openssl s_client -servername pihole.temet.ai -connect pihole.temet.ai:443 2>/dev/null | \
openssl x509 -noout -dates -issuer
# Check all domains
for domain in pihole jaeger langfuse sprinkler ha code webhook; do
echo "=== $domain.temet.ai ==="
echo | openssl s_client -servername $domain.temet.ai -connect $domain.temet.ai:443 2>/dev/null | \
openssl x509 -noout -dates
echo
done
# Check Caddy renewal logs
docker logs caddy 2>&1 | grep -E "renewal|renew|certificate obtained"
Table of Contents
- •When to Use This Skill
- •What This Skill Does
- •Instructions
- •3.1 Check Certificate Expiry
- •3.2 Monitor Auto-Renewal Status
- •3.3 Check Certificate Details
- •3.4 Force Manual Renewal
- •3.5 Backup Certificates
- •3.6 Restore Certificates
- •Supporting Files
- •Expected Outcomes
- •Requirements
- •Red Flags to Avoid
When to Use This Skill
Explicit Triggers:
- •"Check certificate expiry"
- •"Certificate renewal status"
- •"SSL certificate expiring"
- •"Backup certificates"
- •"Force certificate renewal"
Implicit Triggers:
- •Certificate expiring in < 30 days
- •Need to verify auto-renewal working
- •Planning infrastructure maintenance
- •Preparing for disaster recovery
Debugging Triggers:
- •"When does my certificate expire?"
- •"Is auto-renewal working?"
- •"How to backup certificates?"
What This Skill Does
- •Checks Expiry - Verifies certificate validity dates for all domains
- •Monitors Renewal - Reviews Caddy logs for renewal activity
- •Shows Details - Displays certificate issuer, validity, protocols
- •Forces Renewal - Triggers manual certificate renewal if needed
- •Backs Up - Creates backup of caddy_data volume
- •Restores - Restores certificates from backup
Instructions
3.1 Check Certificate Expiry
Check single domain:
echo | openssl s_client -servername pihole.temet.ai -connect pihole.temet.ai:443 2>/dev/null | \ openssl x509 -noout -dates -issuer
Expected output:
notBefore=Jan 10 12:00:00 2026 GMT notAfter=Apr 10 12:00:00 2026 GMT issuer=C = US, O = Let's Encrypt, CN = R3
Check all domains with expiry countdown:
for domain in pihole jaeger langfuse sprinkler ha code webhook; do
echo "=== $domain.temet.ai ==="
cert_info=$(echo | openssl s_client -servername $domain.temet.ai -connect $domain.temet.ai:443 2>/dev/null | \
openssl x509 -noout -dates -issuer 2>&1)
if echo "$cert_info" | grep -q "notAfter"; then
echo "$cert_info"
# Calculate days until expiry
expiry_date=$(echo "$cert_info" | grep notAfter | cut -d= -f2)
expiry_epoch=$(date -j -f "%b %d %T %Y %Z" "$expiry_date" +%s 2>/dev/null || \
date -d "$expiry_date" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( ($expiry_epoch - $now_epoch) / 86400 ))
if [ $days_left -lt 30 ]; then
echo "⚠️ WARNING: Expires in $days_left days (renewal due)"
else
echo "✅ Expires in $days_left days"
fi
else
echo "❌ FAILED to get certificate"
fi
echo
done
Alert thresholds:
- •< 30 days: Renewal due (Caddy triggers at 30 days)
- •< 14 days: Check renewal logs for issues
- •< 7 days: Manual intervention may be needed
3.2 Monitor Auto-Renewal Status
Check recent renewal activity:
docker logs caddy 2>&1 | grep -E "renewal|renew|certificate obtained" | tail -20
Expected indicators:
- •
certificate obtained successfully- New certificate issued - •
certificate renewed- Auto-renewal succeeded - •
checking certificate renewal- Caddy checking expiry
Check renewal schedule:
Caddy checks renewals every 12 hours and renews 30 days before expiry.
Verify renewal configuration:
# Check Caddy is running docker ps | grep caddy # Check Cloudflare DNS plugin loaded docker exec caddy caddy list-modules | grep cloudflare # Verify API token set docker exec caddy env | grep CLOUDFLARE_API_KEY
If no renewal activity and expiry < 30 days:
- •Check Caddy logs for errors (use troubleshoot-https skill)
- •Verify Cloudflare API token valid
- •Consider manual renewal (step 3.4)
3.3 Check Certificate Details
View complete certificate details:
domain="pihole.temet.ai" echo | openssl s_client -servername $domain -connect $domain:443 2>/dev/null | \ openssl x509 -noout -text | grep -A5 "Subject:\|Issuer:\|Validity"
Shows:
- •Subject (domain name)
- •Issuer (Let's Encrypt)
- •Validity period (not before/after dates)
Check certificate chain:
echo | openssl s_client -servername pihole.temet.ai -connect pihole.temet.ai:443 -showcerts 2>/dev/null
Check supported protocols:
docker logs caddy | grep -i "protocol\|http/2\|http/3"
Expected: HTTP/2 and HTTP/3 (QUIC) enabled
3.4 Force Manual Renewal
When to force renewal:
- •Certificate expiring in < 7 days with no auto-renewal
- •Testing renewal process
- •After fixing Cloudflare API token
Option A: Reload Caddy (triggers renewal check)
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Caddy will check expiry and renew if < 30 days remaining.
Option B: Restart Caddy (full renewal check)
docker compose -f /home/dawiddutoit/projects/network/docker-compose.yml restart caddy
Option C: Delete and recreate certificates (last resort)
⚠️ WARNING: Only use if renewal failing and expiry imminent.
# Stop Caddy docker compose -f /home/dawiddutoit/projects/network/docker-compose.yml down caddy # Delete certificate volume docker volume rm network_caddy_data # Recreate volume docker volume create network_caddy_data # Start Caddy (obtains fresh certificates) docker compose -f /home/dawiddutoit/projects/network/docker-compose.yml up -d caddy # Monitor certificate issuance docker logs caddy -f
Watch for: certificate obtained successfully {"identifier": "domain.temet.ai"}
Rate limit warning:
- •Let's Encrypt limit: 50 certificates per domain per week
- •Check usage: https://crt.sh/?q=temet.ai
3.5 Backup Certificates
Why backup:
- •Disaster recovery
- •Infrastructure migration
- •Before risky changes
Note: Certificates can be re-obtained automatically via DNS-01 challenge. Backup not strictly necessary if you have valid Cloudflare API token.
Backup caddy_data volume:
# Create backup directory mkdir -p /home/dawiddutoit/projects/network/backups # Backup with date stamp backup_file="/home/dawiddutoit/projects/network/backups/caddy-backup-$(date +%Y%m%d-%H%M%S).tar.gz" tar -czf "$backup_file" \ -C /var/lib/docker/volumes/network_caddy_data/_data . echo "Backup created: $backup_file" # Check backup size ls -lh "$backup_file"
Backup retention:
- •Keep last 3 backups (certificates change every 60 days)
- •Delete backups older than 6 months
Alternative: Backup entire configuration:
backup_dir="/home/dawiddutoit/projects/network/backups/full-backup-$(date +%Y%m%d)" mkdir -p "$backup_dir" # Backup configuration files cp -r /home/dawiddutoit/projects/network/docker-compose.yml "$backup_dir/" cp -r /home/dawiddutoit/projects/network/caddy "$backup_dir/" cp -r /home/dawiddutoit/projects/network/config "$backup_dir/" # Backup .env (SENSITIVE - secure this file) cp /home/dawiddutoit/projects/network/.env "$backup_dir/.env" # Backup Docker volumes tar -czf "$backup_dir/caddy_data.tar.gz" \ -C /var/lib/docker/volumes/network_caddy_data/_data . echo "Full backup created: $backup_dir"
3.6 Restore Certificates
Restore from backup:
backup_file="/home/dawiddutoit/projects/network/backups/caddy-backup-20260110.tar.gz" # Stop Caddy docker compose -f /home/dawiddutoit/projects/network/docker-compose.yml down caddy # Delete existing volume docker volume rm network_caddy_data # Recreate volume docker volume create network_caddy_data # Restore from backup tar -xzf "$backup_file" \ -C /var/lib/docker/volumes/network_caddy_data/_data # Start Caddy docker compose -f /home/dawiddutoit/projects/network/docker-compose.yml up -d caddy # Verify certificates loaded docker logs caddy --tail 50
Disaster recovery scenario:
If complete infrastructure loss:
- •Restore .env file (contains API tokens)
- •Restore docker-compose.yml
- •Restore Caddyfile
- •Start Caddy (automatically obtains certificates)
No certificate backup needed if Cloudflare API token valid.
Supporting Files
| File | Purpose |
|---|---|
references/reference.md | Let's Encrypt details, DNS-01 challenge, renewal schedules |
scripts/check-expiry.sh | Automated certificate expiry checker |
examples/examples.md | Example certificate checks, backup procedures |
Expected Outcomes
Success:
- •All certificates valid and not expiring soon (> 30 days)
- •Recent renewal activity in logs
- •Backup created successfully
- •Certificates restored and working
Warnings:
- •Certificate expiring in < 30 days (renewal due)
- •No renewal activity in logs (check troubleshoot-https skill)
Failure Indicators:
- •Certificate expired
- •Renewal failing repeatedly
- •No certificate obtained after manual renewal attempt
Requirements
- •Docker running with Caddy container
- •Valid Cloudflare API token for DNS-01 challenge
- •Network connectivity for ACME protocol
- •Sufficient disk space for backups
Red Flags to Avoid
- • Do not delete caddy_data volume without backup (unless can re-obtain quickly)
- • Do not exceed Let's Encrypt rate limits (50 certs/domain/week)
- • Do not force renewal repeatedly (causes rate limiting)
- • Do not restore old certificates if near expiry (let Caddy renew fresh)
- • Do not skip checking Cloudflare API token before manual renewal
- • Do not commit certificate backups to git (includes private keys)
- • Do not backup .env file to insecure location (contains secrets)
Notes
- •Let's Encrypt certificates valid for 90 days
- •Caddy renews automatically 30 days before expiry
- •Renewal checks occur every 12 hours
- •DNS-01 challenge allows internal-only services to get certificates
- •Certificates stored in
/var/lib/docker/volumes/network_caddy_data/_data - •No downtime during renewal (Caddy handles gracefully)
- •OCSP stapling enabled by default (better performance)
- •HTTP/2 and HTTP/3 (QUIC) supported automatically
- •Certificate transparency logs: https://crt.sh/?q=temet.ai