Certbot Commands Reference 2026: certonly, renew, dns-01 & Production Examples
Certbot Commands & Certificate Management Guide (2026)
Section titled “Certbot Commands & Certificate Management Guide (2026)”Certbot commands are the foundation of automated certificate management in 2026. This complete reference covers every essential command — certonly, renew, dns-01, rate-limiting, dry-run testing and production examples — with real output and troubleshooting tips so you can run reliable ACME automation at scale. Separate sections also cover, enterprise fleet management, and preparation for Let’s Encrypt’s shift to short-lived certificates (6-day lifetimes by 2028).
Need help with ACME? Ask Axel Axelspire AI bot with own augmented memory for all ACME/certbot.
Overview
Section titled “Overview”Certbot command patterns determine certificate automation reliability in production environments. While official documentation explains command syntax, production operations require understanding failure modes, enterprise deployment patterns, monitoring integration, and the operational implications of Let’s Encrypt’s evolving certificate lifetime policies. This guide provides the command knowledge needed to build certificate infrastructure that operates reliably at scale.
Production certificate operations face challenges beyond basic command execution: rate limit management across hundreds of domains, automated renewal workflows that handle failures gracefully, certificate distribution to load-balanced fleets, integration with secret management systems, and monitoring that detects problems before outages occur. Understanding Certbot’s complete command surface—including lesser-known flags, hook mechanisms, and diagnostic capabilities—enables operations teams to implement robust certificate automation.
The certificate automation landscape is shifting dramatically with Let’s Encrypt’s planned lifetime reductions: 47-day certificates in February 2027 and 6-day certificates in February 2028. These changes make manual certificate management impossible and require fully automated renewal workflows. This guide emphasizes command patterns that work in the short-lived certificate future, not just today’s 90-day world.
What’s Changed: The 2025–2028 Let’s Encrypt Timeline
Section titled “What’s Changed: The 2025–2028 Let’s Encrypt Timeline”Before diving into commands, understand the ground shifting beneath certificate operations:
| Date | Change | Impact |
|---|---|---|
| Now | No TLS Client Auth EKU in classic profile | Use --preferred-profile tlsclient if you need client auth certificates (deprecated May 13, 2026). If using client auth (e.g., mTLS for SMTP), migrate to dedicated client certs ASAP—public CAs like LE are phasing this out industry-wide by mid-2026. |
| Jan 15, 2026 | 6-day certs + IP addresses available (shortlived profile) | Opt-in for ultra-short testing; automation mandatory |
| May 13, 2026 | Opt-in 45-day certs (tlsserver profile) | Early adopters/test automation |
| Feb 2027 | Default to 64 days (classic profile | Renewal window ~21 days; manual processes break |
| Feb 2028 | Default to 45 days (classic profile) | Renewal window ~15 days; daily automation norm |
LE is shortening faster than industry max (47 days by Mar 2029). Prep for 45-day as the 2028 default.
The operational reality: If your renewal process involves a human touching anything, you have less than a year to fix it. This guide assumes you’re building for the 6-day certificate world.
ACME Protocol: What’s Actually Happening
Section titled “ACME Protocol: What’s Actually Happening”Certbot implements the ACME protocol (RFC 8555). Understanding the protocol—not just the CLI—prevents debugging in the dark and enables effective troubleshooting.
The ACME Flow
Section titled “The ACME Flow”1. Client → CA: "I want a cert for example.com" (newOrder)2. CA → Client: "Prove you control it. Here's a token." (authorization + challenge)3. Client: Places proof (HTTP file or DNS record)4. Client → CA: "Check it now." (respond to challenge)5. CA → Client: "Verified. Here's your cert." (finalize + download)Challenge Types: When to Use What
Section titled “Challenge Types: When to Use What”| Challenge | Mechanism | Ports Required | Wildcard Support | Best For |
|---|---|---|---|---|
| HTTP-01 | Token file at /.well-known/acme-challenge/{token} | 80 inbound | No | Single servers, simple setups |
| DNS-01 | TXT record at _acme-challenge.{domain} | None | Yes | Wildcards, non-web servers, firewalled environments |
| TLS-ALPN-01 | TLS negotiation on port 443 | 443 inbound | No | When port 80 unavailable; niche use |
Decision framework:
- Need
*.example.com? → DNS-01, no alternative - Port 80 open and single server? → HTTP-01, simplest path
- Behind a CDN/load balancer that terminates TLS? → DNS-01 (HTTP-01 often fails due to redirect loops)
- Internal-only server, no public DNS? → DNS-01 with split-horizon or TLS-ALPN-01
Installation
Section titled “Installation”Snap Install (Required for Certbot 5.x)
Section titled “Snap Install (Required for Certbot 5.x)”# Install snapd if not presentsudo apt update && sudo apt install -y snapdsudo snap install core && sudo snap refresh core
# Remove any apt-installed Certbot (critical — mixing causes plugin conflicts)sudo apt remove -y certbot python3-certbot-* 2>/dev/null
# Install Certbot via snapsudo snap install --classic certbotsudo ln -s /snap/bin/certbot /usr/bin/certbot
# Verify installationcertbot --version# Expected: certbot 5.x.xWhy snap, not apt? Ubuntu/Debian apt repositories freeze Certbot at old versions with outdated ACME libraries. Snap provides automatic updates, which matters when Let’s Encrypt changes server-side behavior (and they do, regularly).
DNS Plugin Installation
Section titled “DNS Plugin Installation”Plugins must also come from snap to avoid Python environment conflicts:
# Cloudflare (most common)sudo snap install certbot-dns-cloudflare
# AWS Route 53sudo snap install certbot-dns-route53
# Google Cloud DNSsudo snap install certbot-dns-google
# Other available plugins:# certbot-dns-digitalocean, certbot-dns-linode, certbot-dns-ovh,# certbot-dns-rfc2136 (for BIND/PowerDNS), certbot-dns-azureCommon trap: Installing a plugin via pip when Certbot is via snap (or vice versa) produces “Plugin not found” errors. Always match the installation method.
Core Commands
Section titled “Core Commands”certbot certificates — Inventory
Section titled “certbot certificates — Inventory”# List all managed certificatessudo certbot certificates
# Output includes:# Certificate Name, Domains, Expiry Date, Certificate Path, Private Key Path# CRITICAL: Check "VALID: X days" — anything under 30 days needs attention
# Filter to a specific certificatesudo certbot certificates --cert-name example.comProduction use: Parse this output in monitoring scripts to track certificate expiration across infrastructure.
certbot run — Obtain + Install (Web Server Integration)
Section titled “certbot run — Obtain + Install (Web Server Integration)”# Nginx (auto-configures server blocks)sudo certbot run --nginx \ -d example.com \ -d www.example.com \ --agree-tos \ --email security@example.com \ --redirect \ --hsts \ --staple-ocsp
# Apachesudo certbot run --apache \ -d example.com \ -d www.example.com \ --agree-tos \ --email security@example.comFlag explanations:
--redirect: Adds permanent 301 redirect from HTTP to HTTPS in web server config--hsts: AddsStrict-Transport-Securityheader (hard to undo once deployed)--staple-ocsp: Enables OCSP stapling for faster validation and improved privacy
certbot certonly — Obtain Without Installing
Section titled “certbot certonly — Obtain Without Installing”Use when you manage web server configuration yourself, or for non-web services (mail servers, databases, MQTT brokers).
Webroot mode (web server stays running):
sudo certbot certonly --webroot \ -w /var/www/html \ -d example.com \ -d www.example.comStandalone mode (Certbot runs its own temporary server — port 80 must be free):
# Stop your web server firstsudo systemctl stop nginxsudo certbot certonly --standalone -d example.comsudo systemctl start nginxDNS plugin mode (production wildcard pattern):
sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ --dns-cloudflare-propagation-seconds 30 \ -d "*.example.com" \ -d example.com \ --key-type ecdsa \ --elliptic-curve secp384r1Manual DNS mode (one-off/learning only — not automatable):
sudo certbot certonly --manual \ --preferred-challenges dns \ -d "*.example.com" \ -d example.com
# Certbot will prompt you to create a TXT record# Verify propagation before pressing Enter:dig TXT _acme-challenge.example.com @8.8.8.8 +shortNew in Certbot 5.3.0: IP Address Support
Add IP addresses as SANs (supported by Let’s Encrypt since Jan 15, 2026). Use multiple --ip-address flags for multiple IPs:
sudo certbot certonly --standalone \ -d example.com \ --ip-address 192.0.2.1 \ --ip-address 2001:db8::1certbot renew — Renewal
Section titled “certbot renew — Renewal”# Dry run (always test first)sudo certbot renew --dry-run -v
# Actual renewal (renews all certificates within renewal window)sudo certbot renew
# Force a specific certificate to renew now (regardless of expiry)sudo certbot renew --cert-name example.com --force-renewal
# Renew and allow partial success (if one domain fails, others proceed)sudo certbot renew --allow-subset-of-namesRenewal window: Certbot renews when a certificate has less than 1/3 of its lifetime remaining. For 90-day certificates, that’s ~30 days before expiry. For 47-day certificates (coming Feb 2027), that’s ~15 days. For 6-day certificates (coming Feb 2028), that’s ~2 days.
certbot revoke — Revocation
Section titled “certbot revoke — Revocation”# Revoke by certificate path (most common)sudo certbot revoke \ --cert-path /etc/letsencrypt/live/example.com/fullchain.pem \ --reason keycompromise
# Revoke by certificate namesudo certbot revoke --cert-name example.com --reason keycompromise
# Valid reasons: unspecified, keycompromise, affiliationchanged,# superseded, cessationofoperationAfter revocation: Always reissue immediately, then delete the old certificate:
sudo certbot delete --cert-name example.com# Then re-obtain with certonly or runcertbot delete — Remove Managed Certificate
Section titled “certbot delete — Remove Managed Certificate”sudo certbot delete --cert-name old-domain.com# Removes from /etc/letsencrypt/ entirely — renewal config, archive, live symlinksFile System Layout
Section titled “File System Layout”Understanding where Certbot stores files prevents half of all debugging sessions:
/etc/letsencrypt/├── accounts/ # ACME account keys (one per CA server)│ └── acme-v02.api.letsencrypt.org/├── archive/ # Actual cert files (numbered: cert1.pem, cert2.pem...)│ └── example.com/│ ├── cert1.pem # Current certificate│ ├── chain1.pem # Intermediate CA chain│ ├── fullchain1.pem # cert + chain (what most servers want)│ └── privkey1.pem # Private key├── live/ # Symlinks → latest files in archive/│ └── example.com/│ ├── cert.pem → ../../archive/example.com/cert1.pem│ ├── chain.pem → ...│ ├── fullchain.pem → ...│ ├── privkey.pem → ...│ └── README├── renewal/ # Renewal configuration (one .conf per cert)│ └── example.com.conf├── renewal-hooks/ # Scripts executed during renewal│ ├── pre/ # Before renewal attempt│ ├── deploy/ # After successful renewal│ └── post/ # After renewal attempt (success or failure)└── options-ssl-*.conf # SSL/TLS configuration templatesKey rules:
- Always reference
live/paths in your server configs (they auto-update via symlinks) - Never manually edit files in
archive/— Certbot manages numbering - Back up
accounts/andrenewal/— losing these means re-registering and reconfiguring
Renewal Hooks: Production Patterns
Section titled “Renewal Hooks: Production Patterns”Hooks transform Certbot from a certificate tool into a deployment automation system. Place executable scripts in the appropriate directory under /etc/letsencrypt/renewal-hooks/.
Available Environment Variables
Section titled “Available Environment Variables”| Variable | Available In | Contains |
|---|---|---|
$RENEWED_DOMAINS | deploy/ | Space-separated list of renewed domains |
$RENEWED_LINEAGE | deploy/ | Path to renewed cert (e.g., /etc/letsencrypt/live/example.com) |
$FAILED_DOMAINS | deploy/ | For failure handling (post-5.x) |
Pattern 1: Web Server Reload
Section titled “Pattern 1: Web Server Reload”#!/bin/bashset -euo pipefail
# Validate config before reloading (prevents taking down the server)if nginx -t 2>/dev/null; then systemctl reload nginx echo "[$(date)] Nginx reloaded for: $RENEWED_DOMAINS"else echo "[$(date)] ERROR: Nginx config test failed after cert renewal" >&2 # Send alert via monitoring system exit 1fiPattern 2: Distribute to Multiple Services
Section titled “Pattern 2: Distribute to Multiple Services”#!/bin/bashset -euo pipefail
CERT_DIR="$RENEWED_LINEAGE"
# Copy to Postfix mail servercp "$CERT_DIR/fullchain.pem" /etc/postfix/tls/server.pemcp "$CERT_DIR/privkey.pem" /etc/postfix/tls/server.keychmod 600 /etc/postfix/tls/server.keysystemctl reload postfix
# Copy to HAProxy (requires combined format)cat "$CERT_DIR/fullchain.pem" "$CERT_DIR/privkey.pem" \ > /etc/haproxy/certs/combined.pemchmod 600 /etc/haproxy/certs/combined.pemsystemctl reload haproxy
# Copy to Docker containerdocker cp "$CERT_DIR/fullchain.pem" myapp:/app/certs/docker cp "$CERT_DIR/privkey.pem" myapp:/app/certs/docker exec myapp nginx -s reload
echo "[$(date)] Certificates distributed for: $RENEWED_DOMAINS"Pattern 3: Push to AWS (ACM / S3)
Section titled “Pattern 3: Push to AWS (ACM / S3)”#!/bin/bashset -euo pipefail
CERT="$RENEWED_LINEAGE/cert.pem"CHAIN="$RENEWED_LINEAGE/chain.pem"KEY="$RENEWED_LINEAGE/privkey.pem"DOMAIN=$(echo "$RENEWED_DOMAINS" | awk '{print $1}')
# Upload to ACM (for CloudFront/ALB)aws acm import-certificate \ --certificate fileb://"$CERT" \ --certificate-chain fileb://"$CHAIN" \ --private-key fileb://"$KEY" \ --region us-east-1 \ --tags Key=ManagedBy,Value=certbot Key=Domain,Value="$DOMAIN"
# Backup to S3 with encryptionTIMESTAMP=$(date +%Y%m%d-%H%M%S)aws s3 cp "$RENEWED_LINEAGE/" \ "s3://my-cert-backup/$DOMAIN/$TIMESTAMP/" \ --recursive \ --sse AES256
echo "[$(date)] Pushed $DOMAIN to ACM and backed up to S3"Pattern 4: Notify on Renewal
Section titled “Pattern 4: Notify on Renewal”#!/bin/bashset -euo pipefail
DOMAIN=$(echo "$RENEWED_DOMAINS" | awk '{print $1}')EXPIRY=$(openssl x509 -enddate -noout -in "$RENEWED_LINEAGE/cert.pem" | cut -d= -f2)
# Slack notificationcurl -s -X POST "$SLACK_WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "{ \"text\": \"✅ Certificate renewed: $RENEWED_DOMAINS\nNew expiry: $EXPIRY\" }"
# PagerDuty (resolve any open incidents for this certificate)curl -s -X POST https://events.pagerduty.com/v2/enqueue \ -H 'Content-Type: application/json' \ -d "{ \"routing_key\": \"$PD_ROUTING_KEY\", \"event_action\": \"resolve\", \"dedup_key\": \"cert-expiry-$RENEWED_DOMAINS\" }"Make all hooks executable:
chmod +x /etc/letsencrypt/renewal-hooks/deploy/*.shACME Account Management
Section titled “ACME Account Management”Your ACME account is separate from your certificates. Understanding this prevents confusion during migrations and disaster recovery.
# Register a new account (usually done automatically on first certbot run)sudo certbot register --email security@example.com --agree-tos
# Update contact emailsudo certbot update_account --email new-security@example.com
# Show account informationsudo certbot show_account
# Unregister account (careful — this deauthorizes your account with the CA)sudo certbot unregisterAccount key location: /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/
Migration: To move Certbot to a new server and maintain the same account, copy the entire /etc/letsencrypt/ directory. The account key in accounts/ must match what Let’s Encrypt has on file, or you’ll need to re-register.
Rate Limits: What Will Block You
Section titled “Rate Limits: What Will Block You”Let’s Encrypt enforces rate limits to prevent abuse. Hitting them in production is painful—you can’t issue certificates for up to a week.
| Limit | Value | Reset Window |
|---|---|---|
| Certificates per Registered Domain | 50 per week | Rolling 7-day window |
| Duplicate Certificates | 5 per week | Rolling 7-day window |
| Failed Validations | 5 per hour per account per hostname | Rolling 1-hour window |
| New Orders | 300 per 3 hours per account | Rolling 3-hour window |
| Accounts per IP | 10 per 3 hours | Rolling 3-hour window |
| Pending Authorizations | 300 per account | Cleared on completion |
How to Avoid Rate Limit Problems
Section titled “How to Avoid Rate Limit Problems”1. Use staging for testing — always:
# Staging environment — unlimited issuance, but certificates aren't trustedsudo certbot certonly --staging \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d "*.example.com" -d example.com
# When satisfied, remove --staging and issue for production2. Consolidate SANs instead of issuing separate certificates:
# Bad: 5 separate certificates (burns 5 of your 50/week limit)sudo certbot certonly -d app1.example.comsudo certbot certonly -d app2.example.comsudo certbot certonly -d app3.example.com# ...
# Good: 1 certificate with multiple SANs (burns 1)sudo certbot certonly \ -d app1.example.com \ -d app2.example.com \ -d app3.example.com \ -d app4.example.com \ -d app5.example.com3. Check your current certificate inventory before issuing:
sudo certbot certificates# Look for duplicates — delete unnecessary onessudo certbot delete --cert-name duplicate-certCAA Records: Controlling Who Can Issue Certificates
Section titled “CAA Records: Controlling Who Can Issue Certificates”Certificate Authority Authorization (CAA) DNS records specify which CAs are permitted to issue certificates for your domain. Without them, any CA can issue—this is how misissued certificates happen.
# Check current CAA recordsdig CAA example.com +short
# Example CAA configurations (set in your DNS provider):
# Allow only Let's Encryptexample.com. IN CAA 0 issue "letsencrypt.org"
# Allow Let's Encrypt + one other CAexample.com. IN CAA 0 issue "letsencrypt.org"example.com. IN CAA 0 issue "sectigo.com"
# Allow wildcards only from Let's Encryptexample.com. IN CAA 0 issuewild "letsencrypt.org"
# Send violation reports to security teamexample.com. IN CAA 0 iodef "mailto:security@example.com"Why this matters: If you set CAA records and then try to issue from a different CA (or Let’s Encrypt issues from a different domain than listed), issuance fails. Always verify CAA records before troubleshooting mysterious issuance failures.
Certificate Transparency Monitoring
Section titled “Certificate Transparency Monitoring”Every publicly trusted certificate is logged to Certificate Transparency (CT) logs. Monitor these to detect unauthorized issuance.
# Check CT logs for your domain (via crt.sh)curl -s "https://crt.sh/?q=%25.example.com&output=json" | \ python3 -c "import json, sysfor cert in json.load(sys.stdin): print(f\"{cert['not_before']} {cert['common_name']} issuer={cert['issuer_name']}\")" | sort -r | head -20Automated monitoring options:
- crt.sh RSS/Atom feeds: Subscribe to
https://crt.sh/atom?q=%25.example.com - Facebook Certificate Transparency Monitoring: Free service at developers.facebook.com
- Certspotter (sslmate.com): Free for one domain, alerts on new issuance
- Build your own: Poll the crt.sh JSON API hourly and alert on unexpected issuers
Troubleshooting
Section titled “Troubleshooting”Common Errors and Solutions
Section titled “Common Errors and Solutions”| Error | Root Cause | Diagnostic Steps | Fix |
|---|---|---|---|
Invalid response from /.well-known/acme-challenge/ | HTTP-01 validation failure | curl -v http://example.com/.well-known/acme-challenge/test | Ensure port 80 open; verify webroot path; disable HTTPS redirects for /.well-known/ |
DNS problem: NXDOMAIN looking up TXT for _acme-challenge | TXT record missing or not propagated | dig TXT _acme-challenge.domain @8.8.8.8 +short | Wait for DNS propagation; verify zone; lower TTL to 60s |
DNS problem: SERVFAIL looking up TXT | Authoritative DNS server failure or DNSSEC issue | dig TXT _acme-challenge.domain +dnssec @8.8.8.8 | Check DNSSEC chain; verify authoritative nameserver |
too many certificates already issued for exact set of domains | Duplicate certificate rate limit (5/week) | sudo certbot certificates | Delete duplicates; wait 7 days; consolidate SANs |
too many certificates (50) already issued | Registered domain rate limit | Review all issuance at crt.sh | Wait 7 days; use SANs instead of separate certificates |
too many failed authorizations | Failed validation rate limit (5/hour) | Check /var/log/letsencrypt/letsencrypt.log | Fix underlying validation issue; wait 1 hour |
Plugin not found or Namespace collision | Mixing apt and snap installations | which certbot && snap list certbot | sudo apt remove certbot python3-certbot-*; reinstall via snap |
Could not bind to port 80 | Standalone mode but web server occupying port | sudo ss -tlnp | grep :80 | Stop web server or use --webroot mode instead |
Timeout during connect (likely firewall problem) | Let’s Encrypt cannot reach your server | Check firewall rules for port 80 from external | Open port 80 inbound; check cloud security groups |
The server experienced an internal error | Let’s Encrypt server-side issue | Check letsencrypt.status.io | Retry in 15–30 minutes |
Certificate not yet due for renewal | Certificate outside renewal window | sudo certbot certificates | Use --force-renewal if needed early |
unauthorized: CAA record prevents issuance | CAA DNS record blocks Let’s Encrypt | dig CAA example.com +short | Add 0 issue "letsencrypt.org" CAA record |
Debugging Workflow
Section titled “Debugging Workflow”Renewal fails │ ├─ Check /var/log/letsencrypt/letsencrypt.log │ └─ Identify: challenge failure? rate limit? DNS? connectivity? │ ├─ Challenge failure (HTTP-01)? │ ├─ curl http://yourdomain/.well-known/acme-challenge/test │ ├─ Is port 80 open? (sudo ss -tlnp | grep :80) │ ├─ Is redirect sending Let's Encrypt to HTTPS? (curl -v) │ └─ Is webroot path correct in renewal config? │ ├─ Challenge failure (DNS-01)? │ ├─ dig TXT _acme-challenge.domain @8.8.8.8 +short │ ├─ Is API token still valid? (test manual API call) │ ├─ Did DNS provider change API? (check plugin changelog) │ └─ Is DNSSEC broken? (dig +dnssec) │ ├─ Rate limited? │ ├─ Which limit? (log message tells you) │ ├─ Check crt.sh for recent issuance │ └─ Wait (1hr for failed validations; 7 days for cert limits) │ └─ Everything looks fine but still failing? ├─ Run with --dry-run -v for verbose output ├─ Check Let's Encrypt status page (letsencrypt.status.io) └─ Try --staging to isolate server-side issuesEnterprise Fleet Patterns
Section titled “Enterprise Fleet Patterns”Centralized Issuance with Distribution
Section titled “Centralized Issuance with Distribution”For organizations managing 50+ servers, running Certbot on every box creates operational complexity. Instead, centralize certificate issuance:
┌─────────────────┐ ┌───────────────┐│ Certbot Host │───────→│ Web Server 1 ││ (bastion/CI) │───────→│ Web Server 2 ││ │───────→│ Web Server 3 ││ - DNS plugin │───────→│ Mail Server ││ - All renewals │───────→│ API Gateway ││ - Monitoring │ └───────────────┘└─────────────────┘ │ └──→ S3/Vault (backup + secret store)Ansible Playbook: Renew + Distribute
---- name: Renew certificates on certbot host hosts: certbot_host become: yes tasks: - name: Run Certbot renewal command: certbot renew --deploy-hook "/opt/scripts/cert-distributed.sh" register: renew_result changed_when: "'No renewals were attempted' not in renew_result.stdout"
- name: Check for renewal failures fail: msg: "Certbot renewal failed: {{ renew_result.stderr }}" when: renew_result.rc != 0
- name: Distribute certificates to fleet hosts: webservers become: yes tasks: - name: Sync certificate files from certbot host synchronize: src: "/etc/letsencrypt/live/{{ cert_name }}/" dest: "/etc/ssl/certs/{{ cert_name }}/" mode: push rsync_opts: - "--chmod=D750,F640" notify: reload nginx
- name: Set private key permissions file: path: "/etc/ssl/certs/{{ cert_name }}/privkey.pem" mode: '0600' owner: root group: ssl-cert
handlers: - name: reload nginx service: name: nginx state: reloadedHashiCorp Vault Integration
Section titled “HashiCorp Vault Integration”For organizations using Vault as a secret store, push renewed certificates there instead of distributing files:
#!/bin/bashset -euo pipefail
DOMAIN=$(echo "$RENEWED_DOMAINS" | awk '{print $1}')VAULT_PATH="secret/certs/$DOMAIN"
vault kv put "$VAULT_PATH" \ cert=@"$RENEWED_LINEAGE/cert.pem" \ chain=@"$RENEWED_LINEAGE/chain.pem" \ fullchain=@"$RENEWED_LINEAGE/fullchain.pem" \ privkey=@"$RENEWED_LINEAGE/privkey.pem" \ renewed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ expires="$(openssl x509 -enddate -noout -in "$RENEWED_LINEAGE/cert.pem" | cut -d= -f2)"
echo "[$(date)] Pushed $DOMAIN to Vault at $VAULT_PATH"Consuming services pull from Vault using native integrations (Vault Agent, Vault CSI driver, etc.).
Kubernetes: cert-manager (Not Certbot)
Section titled “Kubernetes: cert-manager (Not Certbot)”In Kubernetes, don’t use Certbot. Use cert-manager, which is purpose-built for the Kubernetes lifecycle and handles renewal, secret rotation, and multi-issuer scenarios natively.
Installation
Section titled “Installation”# Install cert-managerkubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml
# Verify pods are runningkubectl get pods -n cert-manager# Wait for all 3 pods: cert-manager, cert-manager-cainjector, cert-manager-webhookClusterIssuer Configuration
Section titled “ClusterIssuer Configuration”---# Staging (always test first)apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-stagingspec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory email: platform-team@example.com privateKeySecretRef: name: letsencrypt-staging-account solvers: - http01: ingress: ingressClassName: nginx---# ProductionapiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-prodspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: platform-team@example.com privateKeySecretRef: name: letsencrypt-prod-account solvers: # HTTP-01 for standard domains - http01: ingress: ingressClassName: nginx # DNS-01 for wildcards (example: Cloudflare) - dns01: cloudflare: email: dns-admin@example.com apiTokenSecretRef: name: cloudflare-api-token key: api-token selector: dnsNames: - "*.example.com"Certificate Request
Section titled “Certificate Request”apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: example-com-tls namespace: productionspec: secretName: example-com-tls duration: 2160h # 90 days (requested; CA may override) renewBefore: 720h # Renew 30 days before expiry privateKey: algorithm: ECDSA size: 256 dnsNames: - example.com - www.example.com - "*.example.com" issuerRef: name: letsencrypt-prod kind: ClusterIssuerIngress Annotation (Simpler Alternative)
Section titled “Ingress Annotation (Simpler Alternative)”# Annotate Ingress directly for automatic certificate provisioningapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: example-ingress annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod"spec: tls: - hosts: - example.com - www.example.com secretName: example-com-tls rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: example-service port: number: 80Monitoring cert-manager
Section titled “Monitoring cert-manager”# Check certificate statuskubectl get certificates -A
# Detailed certificate informationkubectl describe certificate example-com-tls -n production
# Check certificate requestskubectl get certificaterequests -A
# Check challenges (if stuck)kubectl get challenges -Akubectl describe challenge <challenge-name> -n <namespace>
# View cert-manager logskubectl logs -n cert-manager deploy/cert-manager -fCommon failure: “Waiting for HTTP-01 challenge propagation”
- Ensure your Ingress controller can serve
/.well-known/acme-challenge/paths - Verify ingress class matches ClusterIssuer solver configuration
Monitoring & Alerting
Section titled “Monitoring & Alerting”Prometheus + Blackbox Exporter
Section titled “Prometheus + Blackbox Exporter”The most robust approach: probe your actual endpoints and alert on certificate expiry.
Blackbox Exporter configuration:
modules: https_cert_check: prober: http timeout: 10s http: preferred_ip_protocol: ip4 tls_config: insecure_skip_verify: falsePrometheus scrape configuration:
scrape_configs: - job_name: 'ssl-cert-check' metrics_path: /probe params: module: [https_cert_check] static_configs: - targets: - https://example.com - https://api.example.com - https://app.example.com relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: blackbox-exporter:9115Alert rules:
groups: - name: ssl-certificate-alerts rules: - alert: CertExpiringIn30Days expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30 for: 10m labels: severity: warning annotations: summary: "Certificate expiring within 30 days" description: "{{ $labels.instance }} expires in {{ $value | humanizeDuration }}"
- alert: CertExpiringIn7Days expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7 for: 5m labels: severity: critical annotations: summary: "URGENT: Certificate expiring within 7 days" description: "{{ $labels.instance }} expires in {{ $value | humanizeDuration }}"
- alert: CertExpired expr: probe_ssl_earliest_cert_expiry - time() <= 0 for: 1m labels: severity: page annotations: summary: "OUTAGE: Certificate has expired" description: "{{ $labels.instance }} certificate is expired"Simple Cron-Based Monitoring (No Prometheus)
Section titled “Simple Cron-Based Monitoring (No Prometheus)”For smaller setups without Prometheus:
#!/bin/bash# /opt/scripts/check-certs.sh — run via cron every 6 hoursset -euo pipefail
ALERT_DAYS=14SLACK_WEBHOOK="${SLACK_WEBHOOK_URL:-}"DOMAINS=("example.com" "api.example.com" "app.example.com")
for domain in "${DOMAINS[@]}"; do expiry_epoch=$(echo | openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null \ | cut -d= -f2 \ | xargs -I{} date -d {} +%s 2>/dev/null || echo 0)
if [ "$expiry_epoch" -eq 0 ]; then message="⚠️ Cannot check certificate for $domain" else now=$(date +%s) days_left=$(( (expiry_epoch - now) / 86400 ))
if [ "$days_left" -le 0 ]; then message="🔴 EXPIRED: $domain certificate has expired!" elif [ "$days_left" -le "$ALERT_DAYS" ]; then message="🟡 WARNING: $domain expires in $days_left days" else continue # Certificate is fine, skip fi fi
echo "$message"
if [ -n "$SLACK_WEBHOOK" ]; then curl -s -X POST "$SLACK_WEBHOOK" \ -H 'Content-Type: application/json' \ -d "{\"text\": \"$message\"}" fidoneCron entry (every 6 hours):
0 */6 * * * /opt/scripts/check-certs.sh >> /var/log/cert-check.log 2>&1Security Hardening
Section titled “Security Hardening”Key Type and Cipher Selection
Section titled “Key Type and Cipher Selection”# ECDSA P-384 (recommended — strong + fast)sudo certbot certonly --key-type ecdsa --elliptic-curve secp384r1 -d example.com
# ECDSA P-256 (lighter, perfectly adequate)sudo certbot certonly --key-type ecdsa --elliptic-curve secp256r1 -d example.com
# RSA 4096 (only if you need compatibility with very old clients)sudo certbot certonly --key-type rsa --rsa-key-size 4096 -d example.comRecommendation: Use ECDSA P-256 or P-384. RSA is slower, produces larger handshakes, and offers no security advantage at equivalent strength. The only reason to use RSA is compatibility with clients that don’t support ECDSA (increasingly rare in 2026).
Nginx TLS Hardening
Section titled “Nginx TLS Hardening”ssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;ssl_prefer_server_ciphers off; # Let client choose (modern best practice)
ssl_session_timeout 1d;ssl_session_cache shared:SSL:10m;ssl_session_tickets off; # Disable for forward secrecy
ssl_stapling on;ssl_stapling_verify on;resolver 1.1.1.1 8.8.8.8 valid=300s;resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;add_header X-Content-Type-Options "nosniff" always;Usage in server block:
server { listen 443 ssl http2; server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; include /etc/nginx/snippets/ssl-hardening.conf;
# Application configuration...}SSL Configuration Validation
Section titled “SSL Configuration Validation”# Quick certificate checkopenssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -subject -issuer -dates -ext subjectAltName
# Test live certificateopenssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -text
# SSL Labs scan (comprehensive audit)# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=example.com
# Programmatic testing (testssl.sh)git clone --depth 1 https://github.com/drwetter/testssl.sh.git./testssl.sh/testssl.sh example.comDisaster Recovery
Section titled “Disaster Recovery”Backup Strategy
Section titled “Backup Strategy”#!/bin/bashset -euo pipefail
BACKUP_DIR="/backup/letsencrypt"TIMESTAMP=$(date +%Y%m%d-%H%M%S)DEST="$BACKUP_DIR/letsencrypt-$TIMESTAMP.tar.gz"
mkdir -p "$BACKUP_DIR"
# Back up everything except live/ (which contains only symlinks)tar czf "$DEST" \ --exclude='/etc/letsencrypt/live' \ /etc/letsencrypt/
# Encrypt with GPG (never store private keys in plain backups)gpg --symmetric --cipher-algo AES256 "$DEST"rm "$DEST" # Remove unencrypted version
# Retain last 30 backupsls -t "$BACKUP_DIR"/*.gpg | tail -n +31 | xargs rm -f 2>/dev/null || true
echo "[$(date)] Backup created: ${DEST}.gpg"Schedule daily backups:
# Cron entry0 2 * * * /opt/scripts/backup-letsencrypt.sh >> /var/log/letsencrypt-backup.log 2>&1Recovery Procedure
Section titled “Recovery Procedure”# 1. Restore from encrypted backupgpg --decrypt letsencrypt-20260130-020000.tar.gz.gpg | sudo tar xzf - -C /
# 2. Recreate symlinks in live/sudo certbot certificates# If certificates show up, symlinks were recreated automatically
# 3. Test renewal to verify everything workssudo certbot renew --force-renewal --dry-run
# 4. If account key is lost, re-registersudo certbot register --email security@example.com --agree-tos# Then re-obtain all certificatesPreparing for Short-Lived Certificates
Section titled “Preparing for Short-Lived Certificates”With 6-day certificates becoming the default by February 2028, your infrastructure must handle daily renewals without human intervention. Here’s the readiness checklist:
Automation Readiness Checklist
Section titled “Automation Readiness Checklist”- Renewals are fully automated — no human touches any part of the process
- DNS plugin installed (if using DNS-01) — manual challenges impossible at daily frequency
- Deploy hooks tested — every downstream service reloads automatically
- Monitoring alerts at 50% lifetime — for 6-day certificates, that’s 3 days
- Cron runs at least twice daily — provides retry window if first attempt fails
- Staging tested with —force-renewal — simulates rapid renewal cadence
- Backup/restore tested — can recover certificate infrastructure within 1 hour
- No hardcoded expiry assumptions — no scripts assume 90-day or 30-day windows
- Rate limits understood — 50 certificates/week/domain still applies
- CI/CD can deploy certificate changes — if certificates baked into containers, pipelines must handle daily rebuilds
Cron Configuration for Short-Lived Certificates
Section titled “Cron Configuration for Short-Lived Certificates”# Current (for 90-day certificates): twice daily is sufficient0 3,15 * * * root certbot renew --quiet --deploy-hook "/opt/scripts/deploy-certs.sh"
# For 6-day certificates: run 4x daily with staggered retry0 */6 * * * root certbot renew --quiet --deploy-hook "/opt/scripts/deploy-certs.sh"
# With failure alerting0 */6 * * * root certbot renew --quiet --deploy-hook "/opt/scripts/deploy-certs.sh" || /opt/scripts/alert-renewal-failure.shOpt-In to Short Lifetimes Early (Available May 2026)
Section titled “Opt-In to Short Lifetimes Early (Available May 2026)”# Test with short-lived certificates before they become defaultsudo certbot certonly \ --preferred-profile shortlived \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d example.comMonitor how your infrastructure handles the accelerated renewal cycle before it becomes mandatory.
Alternative ACME Clients
Section titled “Alternative ACME Clients”Certbot isn’t the only option. For specific use cases, alternatives may be better:
| Client | Language | Best For | Key Features |
|---|---|---|---|
| acme.sh | Bash | Minimal environments, no dependencies | Pure bash; 100+ DNS providers |
| lego | Go | Single binary deployment, CI/CD | Static binary; 80+ DNS providers |
| cert-manager | Go | Kubernetes | CRD-based; native K8s integration |
| Caddy | Go | Simple web serving | Built-in ACME; zero-config HTTPS |
| win-acme | C# | Windows/IIS | Native Windows; IIS integration |
Quick Reference Card
Section titled “Quick Reference Card”# ─── ISSUE CERTIFICATES ──────────────────────────────────certbot run --nginx -d example.com # Auto (nginx)certbot certonly --webroot -w /var/www -d example.com # Manual webrootcertbot certonly --dns-cloudflare ... -d *.example.com # Wildcard
# ─── MANAGE CERTIFICATES ─────────────────────────────────certbot certificates # List allcertbot renew --dry-run # Test renewalcertbot renew # Renew all duecertbot renew --cert-name X --force-renewal # Force specificcertbot delete --cert-name X # Remove certificatecertbot revoke --cert-path ... --reason X # Revoke
# ─── VERIFY & DEBUG ──────────────────────────────────────openssl x509 -in cert.pem -noout -dates # Check expiryopenssl s_client -connect host:443 # Test live certdig CAA example.com +short # Check CAAdig TXT _acme-challenge.domain +short # Check DNS-01
# ─── EMERGENCY RESPONSE ──────────────────────────────────certbot revoke --cert-path /path --reason keycompromisecertbot delete --cert-name compromised-certcertbot certonly ... # Re-issue immediatelyRelated Documentation
Section titled “Related Documentation”- Certbot Installation - Install Certbot with plugins across platforms
- Certbot Renewal Automation - Automated renewal with cron and systemd
- HTTP-01 Challenge Overview - HTTP-01 challenge mechanism
- HTTP-01 Challenge Troubleshooting - Resolve validation failures
- DNS-01 Challenge Validation - Wildcard certificates and DNS-01
- Certificate Lifecycle Management - Enterprise certificate operations
- Monitoring and Alerting - Certificate expiration monitoring