Certificate automation isn't optional anymore. With Let's Encrypt certificates lasting just 90 days — and the CA/Browser Forum pushing toward 45-day lifetimes — manual renewal is a ticking time bomb. If you're still renewing certificates by hand, you're one missed calendar reminder away from an outage.
This guide covers the ACME protocol, the major ACME clients, and how to build a renewal pipeline that never lets a certificate expire.
In 2015, certificates lasted 3-5 years. You could buy one, install it, and mostly forget about it. Today:
Shorter certificate lifetimes reduce the window of exposure if a private key is compromised. But they also mean that manual renewal processes will fail — it's a matter of when, not if.
ACME (Automatic Certificate Management Environment) is the protocol that makes automated certificate issuance possible. It was developed by the Internet Security Research Group (ISRG) for Let's Encrypt and standardized as RFC 8555.
Client ACME Server (Let's Encrypt)
| |
|--- 1. Create Account (JWK) ----------->|
|<-- Account URL ------------------------|
| |
|--- 2. Request Certificate (order) ---->|
|<-- Authorization challenges -----------|
| |
|--- 3. Fulfill challenge (prove DNS) -->|
|<-- Challenge validated ----------------|
| |
|--- 4. Submit CSR --------------------->|
|<-- Signed certificate -----------------|
ACME supports three challenge types. Choosing the right one depends on your infrastructure:
| Challenge | How It Works | Use Case | Limitations |
|---|---|---|---|
| HTTP-01 | Place a file at /.well-known/acme-challenge/TOKEN | Simple web servers | Requires port 80 open, no wildcards |
| DNS-01 | Create a _acme-challenge.domain.com TXT record | Wildcards, internal servers, no port 80 | Requires DNS API access |
| TLS-ALPN-01 | Respond on port 443 with a self-signed cert containing the token | When only port 443 is available | Requires port 443, no wildcards |
When to use which:
*.example.com). Also the best choice when your server isn't publicly accessible (internal services, staging environments behind a VPN).Certbot is the reference ACME client, maintained by the EFF. It handles certificate issuance, installation, and renewal.
# Debian/Ubuntu
apt update && apt install certbot
# With Nginx plugin
apt install python3-certbot-nginx
# With Apache plugin
apt install python3-certbot-apache
# RHEL/CentOS/Fedora
dnf install certbot python3-certbot-nginx
The Nginx plugin handles everything — it modifies your Nginx config, obtains the certificate, and configures SSL:
certbot --nginx -d example.com -d www.example.com
This will:
ssl_certificate directivesTip: Use the Nginx Config Generator to create a clean config first, then let Certbot add SSL on top.
When you don't have a web server running (or want Certbot to handle validation itself):
# Temporarily binds to port 80
certbot certonly --standalone -d example.com
Note: You must stop any service using port 80 first (systemctl stop nginx).
For wildcard certificates or servers behind a firewall:
# Install Cloudflare plugin
apt install python3-certbot-dns-cloudflare
# Create credentials file
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini
# Obtain wildcard certificate
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com -d "*.example.com"
Certbot installs a systemd timer that runs twice daily:
# Check timer status
systemctl status certbot.timer
# List upcoming renewals
certbot certificates
# Dry-run (test without actually renewing)
certbot renew --dry-run
# Force renewal of a specific cert
certbot renew --cert-name example.com --force-renewal
You can verify your renewal schedule with the Cron Parser if you're using a cron-based approach instead of systemd timers.
Caddy is the simplest way to get automatic HTTPS. It obtains and renews certificates with zero configuration:
example.com {
reverse_proxy localhost:3000
}
That's it. Caddy will:
For wildcards, add DNS provider configuration:
*.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
reverse_proxy localhost:3000
}
Traefik integrates ACME into its Docker-native routing. Certificates are obtained per-service via container labels:
# docker-compose.yml
services:
traefik:
image: traefik:v3
command:
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "[email protected]"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
volumes:
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
myapp:
image: myapp:latest
labels:
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
volumes:
letsencrypt:
Each new service with the certresolver label automatically gets its own certificate.
If you're not using Certbot's built-in timer (or you need custom post-renewal hooks), create your own:
# /etc/systemd/system/cert-renew.service
[Unit]
Description=Renew Let's Encrypt certificates
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
ExecStartPost=/usr/bin/systemctl reload nginx
# /etc/systemd/system/cert-renew.timer
[Unit]
Description=Run cert renewal twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now cert-renew.timer
Tip: Use the Systemd Service Generator to build these unit files interactively, with security hardening options.
Certbot supports hooks that run after a successful renewal:
# Reload Nginx after renewal
certbot renew --deploy-hook "systemctl reload nginx"
# Reload HAProxy
certbot renew --deploy-hook "systemctl reload haproxy"
# Copy certs and reload a Docker container
certbot renew --deploy-hook "/opt/scripts/deploy-certs.sh"
You can make hooks permanent by placing scripts in /etc/letsencrypt/renewal-hooks/deploy/.
Automation is not a substitute for monitoring. Renewals can fail silently (DNS provider API changes, rate limits, firewall changes). Always monitor:
# Check days until expiry
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -enddate
Tip: Build this command interactively with the OpenSSL Command Builder — select "Check Expiry" under TLS Testing.
#!/bin/bash
DOMAINS="example.com api.example.com app.example.com"
WARN_DAYS=14
for domain in $DOMAINS; do
expiry=$(echo | openssl s_client -connect "$domain:443" -servername "$domain" 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 "$WARN_DAYS" ]; then
echo "WARNING: $domain expires in $days_left days ($expiry)"
else
echo "OK: $domain expires in $days_left days"
fi
done
For production monitoring, use Prometheus with the blackbox exporter to track certificate expiry as a metric:
# prometheus.yml scrape config
- job_name: 'ssl_expiry'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://example.com
- https://api.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: blackbox-exporter:9115
Alert when certificates are within 14 days of expiry:
# alerting rule
- alert: SSLCertExpiringSoon
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 14
labels:
severity: warning
annotations:
summary: "SSL certificate expiring soon on {{ $labels.instance }}"
Apple's push for 45-day certificate lifetimes (adopted by the CA/Browser Forum) means:
If your certificate management isn't fully automated today, it needs to be before these timelines take effect.
| Feature | Certbot | Caddy | Traefik | acme.sh | Lego |
|---|---|---|---|---|---|
| Language | Python | Go | Go | Shell | Go |
| Auto-renewal | Timer | Built-in | Built-in | Cron | External |
| Web server integration | Nginx, Apache | Built-in | Built-in | Manual | Manual |
| DNS providers | 20+ | 100+ | 30+ | 150+ | 100+ |
| Wildcard support | Yes (DNS-01) | Yes | Yes | Yes | Yes |
| Docker-native | No | Yes | Yes | No | No |
| Complexity | Low | Very low | Medium | Medium | Low |
| Best for | Traditional servers | Simple deployments | Docker/K8s | Shell scripting | Go toolchains |
Certificate automation with ACME is the foundation of modern TLS management. The protocol is straightforward — prove domain ownership, get a certificate — and mature clients like Certbot, Caddy, and Traefik make implementation simple.
Key takeaways:
For hands-on practice: