Automating TLS Certificate Lifecycle with Let's Encrypt and ACME
Your production site just went down at 3 AM because someone forgot to renew a certificate. Again. The manual renewal process that worked fine for two servers has become a liability now that you’re managing fifty. Every quarter, the same ritual: calendar reminders, SSH sessions, certbot commands, nginx reloads, and the lingering anxiety that you missed one. Until you did.
Certificate expiration is the silent killer of uptime. It doesn’t trigger your APM alerts. Load balancers report the backend as healthy right up until browsers start throwing ERR_CERT_DATE_INVALID. By the time your on-call engineer figures out what’s happening, customers have already screenshot the security warning and posted it to Twitter.
The fundamental issue isn’t negligence—it’s that manual processes decay. The engineer who set up the original certificates left the company. The renewal documentation lives in a Confluence page that hasn’t been updated since 2019. The cron job that was supposed to handle this silently failed six months ago because someone rotated the service account credentials.
Let’s Encrypt changed the economics of TLS certificates, but their 90-day validity window was a deliberate design choice, not a limitation. Short-lived certificates force automation. They’re worthless to attackers who compromise your private key three months later. But that 90-day window also means you’re running a renewal process four times more often than traditional certificates—and four times more opportunities for failure.
The ACME protocol that powers Let’s Encrypt wasn’t just built for free certificates. It was built for machines to manage certificates without human intervention. The question isn’t whether to automate certificate management—it’s how to build automation that survives infrastructure changes, team turnover, and the inevitable edge cases that break naive implementations.
Why Certificate Management Breaks at Scale
TLS certificate management seems straightforward until it isn’t. A single certificate renewal takes minutes. Managing hundreds of certificates across distributed services while maintaining zero downtime requires a fundamentally different approach.

The 90-Day Cliff
Let’s Encrypt certificates expire every 90 days by design. This short validity period limits the damage from compromised certificates and encourages automation. However, it creates a relentless operational cadence that exposes weaknesses in manual processes.
Consider a team managing 50 services across three environments. That’s 150 certificates requiring renewal every quarter—roughly 1.6 renewals per day on average. Miss a few during a sprint crunch or holiday period, and the backlog compounds rapidly.
Institutional Knowledge Walks Out the Door
Manual renewal processes depend on tribal knowledge. The engineer who originally configured the certificate knows which DNS provider holds the validation records, which load balancer needs the updated certificate, and which downstream services require restarts.
When that engineer changes teams or leaves the company, the knowledge evaporates. Six months later, a certificate expires at 2 AM, and the on-call engineer faces a production outage with no documentation and no context.
Silent Failures Cascade Into Outages
Certificate renewals fail silently in ways that don’t surface until expiration. A DNS provider API token expires. A firewall rule blocks the validation server. A configuration drift causes the renewal script to write certificates to the wrong path.
These failures don’t trigger alerts. The certificate continues working until it doesn’t. Then services start failing TLS handshakes, health checks cascade, and an expired certificate becomes a P1 incident.
💡 Pro Tip: The time between a silent renewal failure and the resulting outage is your exposure window. With 90-day certificates and 30-day renewal windows, you have roughly 60 days of false confidence before discovering the problem.
ACME: Automation by Design
The ACME (Automatic Certificate Management Environment) protocol addresses these systemic issues at the protocol level. Rather than bolting automation onto a manual process, ACME treats certificate issuance as a machine-to-machine transaction.
ACME clients like Certbot handle the entire lifecycle: generating keys, proving domain control, obtaining certificates, and scheduling renewals. The protocol’s challenge-response mechanism standardizes domain validation, eliminating provider-specific integrations.
Let’s Encrypt processes over 4 million certificate requests daily using ACME. The protocol is battle-tested at scale, and understanding its mechanics is essential for building reliable certificate automation.
ACME Protocol Deep Dive: Domain Validation Under the Hood
Before automating certificate renewals, you need to understand what happens during the ACME handshake. The Automatic Certificate Management Environment (ACME) protocol defines how clients prove domain ownership to Certificate Authorities like Let’s Encrypt—and the challenge type you choose has significant implications for your architecture.

The Challenge-Response Dance
When your ACME client requests a certificate, Let’s Encrypt doesn’t simply trust that you own the domain. The CA issues a challenge: prove control over the domain through one of several validation methods. Your client must respond correctly before any certificate is issued.
The cryptographic flow works as follows:
- Your client generates an account key pair and registers with the CA
- The client submits a certificate signing request (CSR) for your domain
- Let’s Encrypt responds with a challenge token and your account thumbprint
- Your client creates a key authorization by combining these values
- The CA verifies your response and issues the certificate
This entire exchange uses JSON Web Signatures (JWS) to ensure authenticity. Every request from your client is signed with your account private key, preventing impersonation attacks.
HTTP-01 vs DNS-01: Choosing Your Validation Method
HTTP-01 challenges require placing a file at /.well-known/acme-challenge/<token> on your web server. Let’s Encrypt makes an HTTP request to port 80 of your domain to verify the file contents. This method works well for publicly accessible web servers with straightforward deployments.
DNS-01 challenges require creating a TXT record at _acme-challenge.yourdomain.com containing the key authorization. The CA queries DNS to validate domain control.
Use HTTP-01 when:
- You have direct control over the web server
- Your infrastructure allows inbound connections on port 80
- You’re issuing certificates for individual hostnames
Use DNS-01 when:
- You need wildcard certificates (DNS-01 is the only option)
- Your servers sit behind firewalls or load balancers that complicate HTTP validation
- You’re managing certificates for internal services without public HTTP endpoints
- You want to centralize certificate issuance separate from your web infrastructure
💡 Pro Tip: DNS-01 challenges don’t require your servers to be publicly accessible at all. You can issue certificates for internal hostnames as long as you control the public DNS zone—a powerful pattern for zero-trust architectures.
Why Wildcards Demand DNS Validation
Wildcard certificates (*.example.com) present a unique security consideration. HTTP-01 validation only proves control over a specific hostname, but wildcards grant trust for any subdomain. Allowing HTTP-01 for wildcards would let an attacker who compromises a single subdomain obtain a certificate valid for your entire domain.
DNS-01 requires control over the authoritative DNS zone itself—a much stronger proof of domain ownership that justifies the broader certificate scope.
Respecting Rate Limits
Let’s Encrypt enforces rate limits to protect their infrastructure:
- 50 certificates per registered domain per week
- 5 duplicate certificates per week (same exact hostname set)
- 5 failed validations per account per hostname per hour
- 300 new orders per account per 3 hours
In production, these limits become critical during incident recovery. If a misconfiguration triggers repeated failed validations, you can lock yourself out for an hour. The duplicate certificate limit catches teams who accidentally request the same certificate repeatedly instead of reusing existing ones.
Staging environments should always use Let’s Encrypt’s staging endpoint (acme-staging-v02.api.letsencrypt.org), which has significantly higher limits for testing.
With this understanding of how ACME validation works, you’re ready to configure Certbot for production use—including the renewal automation that makes this protocol truly powerful.
Setting Up Certbot for Automated Renewal
Certbot remains the most widely deployed ACME client, and for good reason: it handles the complexity of certificate acquisition, storage, and renewal while integrating cleanly with major web servers. Getting it configured correctly from the start prevents the 3 AM pages that come from expired certificates.
Installation and Initial Configuration
On modern Debian-based systems, install Certbot through snap to ensure you’re running the latest version with all security patches:
sudo snap install --classic certbotsudo ln -s /snap/bin/certbot /usr/bin/certbotFor RHEL-based systems, use the EPEL repository:
sudo dnf install epel-releasesudo dnf install certbot python3-certbot-nginxFor your first certificate, use the standalone mode to verify everything works before integrating with your web server:
sudo certbot certonly --standalone \ -d api.example.com \ --agree-tos \ --no-eff-emailCertbot stores certificates under /etc/letsencrypt/live/api.example.com/, creating symlinks that always point to the current certificate. This design means your applications reference a stable path while Certbot handles the underlying file rotation. The directory contains four key files: privkey.pem (your private key), fullchain.pem (certificate plus intermediates), cert.pem (domain certificate only), and chain.pem (intermediate certificates). Most applications need fullchain.pem and privkey.pem.
Configuring Automatic Renewal with systemd Timers
The snap installation includes a systemd timer, but you should verify it’s active and understand its behavior:
sudo systemctl list-timers | grep certbotsudo systemctl status snap.certbot.renew.timerFor non-snap installations or custom renewal schedules, create your own timer:
[Unit]Description=Run Certbot renewal twice daily
[Timer]OnCalendar=*-*-* 00,12:00:00RandomizedDelaySec=3600Persistent=true
[Install]WantedBy=timers.targetThe corresponding service unit defines what the timer executes:
[Unit]Description=Certbot Renewal ServiceAfter=network-online.target
[Service]Type=oneshotExecStart=/usr/bin/certbot renew --quietThe RandomizedDelaySec spreads renewal attempts across your infrastructure, preventing thundering herd problems when managing hundreds of servers. The Persistent directive ensures missed renewals run immediately after system boot.
sudo systemctl daemon-reloadsudo systemctl enable --now certbot-renewal.timerPost-Renewal Hooks for Service Reloading
Certificates are useless until your services load them. Certbot’s hook system executes scripts at specific points in the renewal lifecycle:
#!/bin/bashset -euo pipefail
## Log renewal for audit traillogger -t certbot "Certificate renewed for ${RENEWED_DOMAINS}"
## Reload nginx without dropping connectionssystemctl reload nginx
## Reload any services using the certificatesystemctl reload haproxy 2>/dev/null || true
## Signal applications via HUPif pgrep -x "gunicorn" > /dev/null; then pkill -HUP gunicornfiMake the hook executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.shCertbot provides three hook directories: pre (before renewal attempts), deploy (after successful renewal), and post (after all renewal attempts complete). Use deploy hooks for service reloads since they only fire when certificates actually change. The pre hooks are useful for temporarily stopping services that bind to port 80 during standalone validation, while post hooks handle cleanup regardless of renewal success or failure.
Handling Multiple Domains and Certificates
Production environments typically manage certificates for multiple services. Structure your certificate strategy around service boundaries rather than cramming everything into one certificate:
## Primary web applicationsudo certbot certonly --webroot \ -w /var/www/html \ -d example.com \ -d www.example.com
## API gateway (separate certificate for independent renewal)sudo certbot certonly --webroot \ -w /var/www/api \ -d api.example.com \ -d api-v2.example.com
## Internal dashboardsudo certbot certonly --standalone \ -d dashboard.internal.example.com \ --preferred-challenges httpEach certificate renews independently, so a validation failure on one domain doesn’t cascade to others. This isolation is particularly valuable when different teams own different services—a DNS misconfiguration for one service won’t block certificate renewal across your entire infrastructure.
Review your renewal configuration:
sudo certbot certificatesThis displays all managed certificates, their domains, expiration dates, and the renewal configuration path. For each certificate, Certbot maintains a renewal configuration file in /etc/letsencrypt/renewal/ that you can edit directly for advanced options like specifying a different authenticator or adjusting RSA key size.
💡 Pro Tip: Run
certbot renew --dry-runafter any configuration changes. This simulates the entire renewal process against Let’s Encrypt’s staging servers without consuming your rate limit.
Test your complete renewal pipeline monthly. The dry-run validates ACME communication, but only a real renewal exercises your deploy hooks and service reload logic. Consider adding monitoring for certificate expiration dates as a safety net—tools like Prometheus with the blackbox exporter can alert you days before expiration if automated renewal fails silently.
With Certbot handling standard HTTP-01 validation, the next challenge is certificates for services that aren’t publicly accessible—internal APIs, private dashboards, and wildcard certificates that cover entire subdomains.
DNS-01 Challenges for Internal Services and Wildcards
HTTP-01 validation works well for public-facing services, but production infrastructure often includes internal APIs, private microservices, and wildcard domains that never expose port 80 to the internet. DNS-01 challenges solve this by proving domain ownership through DNS TXT records, enabling certificate issuance for any service you control—regardless of network accessibility.
Why DNS-01 Matters for Production Infrastructure
DNS-01 validation offers two capabilities HTTP-01 cannot provide: wildcard certificates and validation without inbound connectivity. A single wildcard certificate for *.internal.example.com covers every microservice in your cluster without individual certificate management. Internal services behind firewalls, VPNs, or private networks obtain valid certificates without exposing endpoints.
Consider a Kubernetes cluster running dozens of internal services: payment processors communicating with fraud detection, inventory systems synchronizing with fulfillment APIs, and monitoring dashboards aggregating metrics. Each service requires TLS for mTLS enforcement and compliance requirements. Managing individual certificates for each becomes operationally burdensome. A wildcard certificate simplifies deployment while maintaining security posture.
The tradeoff is complexity. DNS-01 requires API access to your DNS provider, introduces propagation delays, and creates a credential management challenge. These are solvable problems with proper automation.
Configuring DNS Provider Credentials
Certbot supports dozens of DNS providers through plugins. Each provider requires API credentials with permission to create and delete TXT records. Store these credentials outside your certificate automation scripts using environment variables or secrets management.
For Cloudflare, create an API token with Zone:DNS:Edit permissions scoped to specific zones:
## Create credentials file with restricted permissionsmkdir -p /etc/letsencrypt/secretscat > /etc/letsencrypt/secrets/cloudflare.ini << EOFdns_cloudflare_api_token = dQw4w9WgXcQ_8kLmNpR2vT5xYzAbCdEfGhIjKlMnOpEOF
chmod 600 /etc/letsencrypt/secrets/cloudflare.inichown root:root /etc/letsencrypt/secrets/cloudflare.ini💡 Pro Tip: Use API tokens instead of global API keys. Tokens provide granular permissions and can be revoked individually without affecting other integrations.
For AWS Route 53, configure IAM credentials with a policy allowing route53:ChangeResourceRecordSets and route53:GetChange on your hosted zones. The certbot Route 53 plugin reads credentials from standard AWS credential chains, including instance profiles for EC2-based automation. When running on EC2 instances, attach an IAM role directly to the instance rather than storing static credentials on disk.
For Google Cloud DNS, create a service account with the dns.admin role scoped to your project. Export the JSON key file and reference it in your certbot configuration. Rotate these credentials quarterly and audit access logs for unauthorized usage patterns.
Obtaining Wildcard Certificates
Wildcard certificates require DNS-01 validation—Let’s Encrypt enforces this as a security measure. Request wildcards by prefixing the domain with *.:
certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \ --dns-cloudflare-propagation-seconds 30 \ -d "*.internal.example.com" \ -d "internal.example.com" \ --non-interactive \ --agree-tos \Include both the wildcard and the bare domain. A certificate for *.internal.example.com does not cover internal.example.com itself—these are distinct names requiring separate validation.
The --dns-cloudflare-propagation-seconds flag tells Certbot how long to wait after creating the TXT record before requesting validation. DNS propagation varies by provider; 30-60 seconds works for most, but some require 120 seconds or more. Monitor your initial requests and adjust this value based on observed propagation times in your environment.
Handling Services Without Public Endpoints
Internal services present unique deployment challenges. The certificate issuance process runs independently of the service itself, meaning you can obtain certificates from a bastion host, CI/CD pipeline, or dedicated certificate management server. Distribute the resulting certificates to internal services through your configuration management system—Ansible, Puppet, or Kubernetes secrets.
For air-gapped environments, consider a dedicated certificate management host with controlled DNS API access. This host handles all certificate operations and pushes renewed certificates to internal services through secure channels, maintaining separation between certificate automation and production workloads.
Automating Record Cleanup
Certbot’s DNS plugins handle record cleanup automatically after validation succeeds. However, failed validations can leave orphaned _acme-challenge TXT records. Implement periodic cleanup as part of your maintenance automation:
#!/bin/bash## Remove stale ACME challenge records older than 1 hour## Run via cron: 0 * * * * /usr/local/bin/cleanup-acme-records.sh
ZONE_ID="a1b2c3d4e5f6g7h8i9j0"API_TOKEN="dQw4w9WgXcQ_8kLmNpR2vT5xYzAbCdEfGhIjKlMnOp"
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=TXT&name=_acme-challenge" \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Content-Type: application/json" | \ jq -r '.result[] | select(.created_on < (now - 3600 | todate)) | .id' | \ xargs -I {} curl -s -X DELETE \ "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/{}" \ -H "Authorization: Bearer ${API_TOKEN}"Log cleanup operations and alert on unexpected record accumulation, which may indicate failing certificate renewals requiring investigation.
With DNS-01 validation operational, certificate provisioning handles your entire infrastructure. The next challenge is knowing when renewals fail before certificates expire—which requires proactive monitoring and alerting.
Monitoring Certificate Expiry and Renewal Failures
Automated renewal means nothing if you don’t know when it fails. A certificate expiring at 3 AM because your renewal job silently crashed for three weeks is entirely preventable—with proper observability. This section covers building monitoring that catches certificate issues before they become outages.
Exposing Certificate Metrics
The foundation of certificate monitoring is exposing expiry data in a format your monitoring stack can consume. The following exporter checks certificates from multiple sources and exposes Prometheus-compatible metrics:
import sslimport socketfrom datetime import datetimefrom prometheus_client import start_http_server, Gaugeimport timeimport logging
logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)
CERT_EXPIRY_SECONDS = Gauge( 'cert_expiry_seconds', 'Seconds until certificate expires', ['domain', 'issuer'])
CERT_RENEWAL_FAILURES = Gauge( 'cert_renewal_failure', 'Certificate renewal failure (1=failed, 0=ok)', ['domain', 'reason'])
def check_certificate(hostname: str, port: int = 443) -> dict: context = ssl.create_default_context() with socket.create_connection((hostname, port), timeout=10) as sock: with context.wrap_socket(sock, server_hostname=hostname) as ssock: cert = ssock.getpeercert()
expiry = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z') issuer = dict(x[0] for x in cert['issuer']).get('organizationName', 'unknown') seconds_remaining = (expiry - datetime.utcnow()).total_seconds()
return { 'domain': hostname, 'issuer': issuer, 'seconds_remaining': seconds_remaining, 'expiry': expiry.isoformat() }
def monitor_domains(domains: list[str], interval: int = 300): while True: for domain in domains: try: result = check_certificate(domain) CERT_EXPIRY_SECONDS.labels( domain=result['domain'], issuer=result['issuer'] ).set(result['seconds_remaining']) CERT_RENEWAL_FAILURES.labels( domain=domain, reason='none' ).set(0) logger.info(f"{domain}: {result['seconds_remaining']/86400:.1f} days remaining") except Exception as e: logger.error(f"Failed to check {domain}: {e}") CERT_RENEWAL_FAILURES.labels( domain=domain, reason=type(e).__name__ ).set(1) time.sleep(interval)
if __name__ == '__main__': start_http_server(9117) monitor_domains(['api.mycompany.io', 'dashboard.mycompany.io', 'auth.mycompany.io'])Alerting Rules
With metrics exposed, configure Prometheus alerting rules that give your team adequate response time. The key is establishing a tiered alerting strategy: warnings should trigger with enough lead time for non-urgent remediation, while critical alerts demand immediate action.
groups: - name: certificate_alerts rules: - alert: CertificateExpiringSoon expr: cert_expiry_seconds < 604800 # 7 days for: 1h labels: severity: warning annotations: summary: "Certificate for {{ $labels.domain }} expires in {{ $value | humanizeDuration }}" runbook_url: https://runbooks.mycompany.io/cert-renewal
- alert: CertificateExpiryCritical expr: cert_expiry_seconds < 259200 # 3 days for: 15m labels: severity: critical annotations: summary: "URGENT: {{ $labels.domain }} certificate expires in {{ $value | humanizeDuration }}"
- alert: CertificateRenewalFailed expr: cert_renewal_failure == 1 for: 30m labels: severity: warning annotations: summary: "Certificate renewal failing for {{ $labels.domain }}: {{ $labels.reason }}"💡 Pro Tip: Set your warning threshold to at least 2x your renewal window. If certificates renew at 30 days remaining, alert at 7 days—this gives you time to handle DNS issues, rate limits, or authentication failures.
Grafana Dashboard Integration
Metrics alone aren’t sufficient without visualization. A well-designed Grafana dashboard provides at-a-glance awareness of your certificate estate and historical renewal patterns. Create a dashboard that displays certificate expiry timelines, renewal success rates over time, and a table highlighting certificates expiring within your warning threshold.
Configure your dashboard to pull from the cert_expiry_seconds metric and display results in days rather than seconds for readability. Add annotations that mark renewal events, making it easy to correlate certificate refreshes with deployment activities. For organizations managing hundreds of certificates, consider grouping panels by environment (production, staging, development) or by certificate authority to quickly identify systemic issues with specific issuers.
Parsing Certbot Logs
Your exporter should also monitor local renewal attempts. Parse Certbot’s log output to detect failures before they compound:
import refrom pathlib import Path
def parse_renewal_log(log_path: str = '/var/log/letsencrypt/letsencrypt.log') -> list[dict]: failures = [] failure_pattern = re.compile( r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*Renewal of (\S+) failed' )
log_content = Path(log_path).read_text() for match in failure_pattern.finditer(log_content): failures.append({ 'timestamp': match.group(1), 'domain': match.group(2), }) return failuresIntegrate this parser with your metrics exporter to push failure counts into Prometheus. This creates a feedback loop where both external certificate checks and internal renewal process health contribute to your overall observability picture.
Runbook Essentials
Your alerting is only as good as your response procedures. Every certificate alert should link to a runbook covering:
- Rate limit exceeded: Check
https://crt.sh/?q=yourdomain.comfor recent issuance, wait or use staging endpoint - DNS validation timeout: Verify TXT record propagation with
dig TXT _acme-challenge.domain.com @8.8.8.8 - Authorization expired: Clear
/etc/letsencrypt/live/and re-run full issuance - Account key mismatch: Restore account credentials from backup or register new account
- HTTP-01 challenge unreachable: Verify firewall rules allow inbound connections on port 80 from Let’s Encrypt validation servers
Document escalation paths in your runbooks. If the on-call engineer cannot resolve a renewal failure within 30 minutes, the runbook should specify who to contact and what information to gather. Include commands for manual certificate issuance as a fallback, ensuring your team can recover even when automation completely fails.
With monitoring in place for individual certificates, the next challenge becomes managing this across dozens or hundreds of services—which requires rethinking certificate management as infrastructure rather than configuration.
Scaling Certificate Management Across Distributed Systems
Managing TLS certificates for a single server is straightforward. Managing them across dozens of services, multiple regions, and hundreds of edge nodes requires deliberate architectural decisions. The choice between centralized and distributed certificate management shapes your operational complexity, failure domains, and recovery capabilities. Organizations that treat certificate management as an afterthought often discover its importance only during an outage—when an expired certificate brings down critical services.
Centralized vs Distributed Strategies
In a centralized model, a single service handles all ACME interactions, stores certificates, and distributes them to consuming services. This approach simplifies rate limit management, provides a single audit point, and reduces the attack surface for private keys. The tradeoff is a single point of failure and added latency in certificate distribution. Centralized systems also require robust internal distribution mechanisms, as certificate consumers depend entirely on the central authority’s availability.
A distributed model allows each service or node to manage its own certificates independently. This eliminates distribution complexity and provides better fault isolation—one service’s certificate failure doesn’t cascade to others. However, you lose visibility into overall certificate health and risk hitting rate limits if renewals aren’t coordinated. Distributed approaches also complicate compliance auditing, as certificate metadata is scattered across your infrastructure.
Most production environments benefit from a hybrid approach: centralized management for shared domains and wildcard certificates, with distributed management for service-specific certificates that don’t require cross-service coordination. This pattern balances operational simplicity with resilience, letting teams maintain autonomy while preserving organizational oversight of shared resources.
Preventing Thundering Herd During Renewals
When certificates share similar issuance dates, renewal attempts cluster together. At scale, this creates rate limit pressure and amplifies the impact of transient ACME endpoint failures. A burst of simultaneous renewals can exhaust your Let’s Encrypt rate limit quota, leaving some certificates unable to renew until the limit resets.
Introduce jitter into renewal scheduling by randomizing the renewal window. Rather than renewing exactly 30 days before expiry, spread renewals across a wider window:
apiVersion: batch/v1kind: CronJobmetadata: name: certificate-renewal namespace: cert-managementspec: schedule: "0 */6 * * *" jobTemplate: spec: template: spec: containers: - name: renewer image: certbot/certbot:v2.11.0 command: - /bin/sh - -c - | # Add random delay (0-3600 seconds) to prevent thundering herd sleep $((RANDOM % 3600)) certbot renew --deploy-hook /scripts/distribute-cert.sh restartPolicy: OnFailureBeyond jitter, consider staggering initial certificate issuance across different days when onboarding new services. This prevents renewal clustering from forming in the first place.
Certificate Distribution to Edge Nodes
After renewal, certificates must reach load balancers and edge nodes quickly. A push-based model using configuration management or a secrets synchronization tool provides faster propagation than polling-based approaches. The distribution mechanism you choose directly impacts how quickly you can recover from certificate issues—faster propagation means shorter outage windows when emergency re-issuance is required.
For Kubernetes environments, cert-manager handles both issuance and distribution seamlessly:
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-productionspec: acme: server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-account-key solvers: - dns01: cloudDNS: project: my-gcp-project serviceAccountSecretRef: name: clouddns-service-account key: credentials.jsonapiVersion: cert-manager.io/v1kind: Certificatemetadata: name: wildcard-example-com namespace: istio-systemspec: secretName: wildcard-example-com-tls issuerRef: name: letsencrypt-production kind: ClusterIssuer dnsNames: - "*.example.com" - "example.com" duration: 2160h # 90 days renewBefore: 720h # 30 daysCert-manager stores certificates as Kubernetes secrets, which Ingress controllers and service meshes consume directly. For multi-cluster deployments, tools like external-secrets-operator or Vault can synchronize certificates across cluster boundaries. When designing cross-cluster synchronization, account for network partitions—each cluster should cache certificates locally to survive temporary connectivity loss to the source of truth.
💡 Pro Tip: When using cert-manager with multiple Ingress controllers, annotate your Certificate resources to specify which controller should handle the HTTP-01 challenge. Misrouted challenge requests are a common source of validation failures.
Distributing to Non-Kubernetes Infrastructure
For load balancers and edge nodes outside Kubernetes, implement a certificate distribution pipeline. Store renewed certificates in a secrets manager (HashiCorp Vault, AWS Secrets Manager), then use configuration management to pull and deploy them. This separation ensures the ACME client never needs direct access to production edge infrastructure, limiting the blast radius if the renewal system is compromised.
Consider implementing certificate versioning in your distribution pipeline. Maintaining the previous certificate version alongside the current one enables rapid rollback if the new certificate causes unexpected issues—such as intermediate chain problems that only manifest on certain client platforms.
Even with robust distribution mechanisms, certificates will occasionally fail to renew or deploy. The next section covers failure detection, alerting, and recovery strategies to maintain continuous TLS coverage.
Handling Failures and Recovery Strategies
Certificate renewal failures at 3 AM create the worst kind of incidents—silent until they become catastrophic. Building resilient renewal pipelines requires understanding failure modes, implementing intelligent retry logic, and establishing clear emergency procedures.
Common Failure Modes
ACME renewals fail for predictable reasons. Rate limits trigger when you exceed 50 certificates per registered domain per week, or 5 failed validation attempts per account per hostname per hour. DNS propagation delays cause validation timeouts, especially with providers that have slow API response times. Network partitions between your infrastructure and Let’s Encrypt’s validation servers create transient failures that resolve themselves.
The most insidious failures come from configuration drift—load balancer rules that no longer route /.well-known/acme-challenge/ correctly, or DNS credentials that expired without notification.
Implementing Retry Logic with Exponential Backoff
Certbot’s built-in retry mechanism handles transient failures, but production systems need wrapper scripts with smarter backoff:
#!/bin/bashset -euo pipefail
MAX_ATTEMPTS=5BASE_DELAY=300 # 5 minutes
attempt=1while [ $attempt -le $MAX_ATTEMPTS ]; do if certbot renew --deploy-hook "/etc/letsencrypt/renewal-hooks/deploy/reload-services.sh" 2>&1; then echo "Renewal successful on attempt $attempt" exit 0 fi
delay=$((BASE_DELAY * (2 ** (attempt - 1)))) # Cap at 2 hours to stay within rate limit windows delay=$((delay > 7200 ? 7200 : delay))
echo "Attempt $attempt failed. Retrying in ${delay}s..." sleep $delay ((attempt++))done
## All retries exhausted—trigger emergency alertcurl -X POST "https://alerts.example.com/webhook/cert-renewal-failed" \ -H "Content-Type: application/json" \ -d '{"severity":"critical","service":"certbot","host":"'"$(hostname)"'"}'
exit 1Emergency Procedures
When automation fails completely, you need a documented manual recovery path. Keep emergency procedures in a runbook accessible outside your primary infrastructure:
#!/bin/bash## Emergency manual renewal - run from bastion host
DOMAIN="${1:?Usage: $0 domain.example.com}"
## Force renewal with verbose output for debuggingcertbot certonly \ --manual \ --preferred-challenges dns \ -d "$DOMAIN" \ --force-renewal \ --verbose
## Verify the new certificateopenssl x509 -in "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" -noout -dates💡 Pro Tip: Store backup certificates with longer validity periods from a secondary CA for genuine emergencies. A 90-day Let’s Encrypt certificate failing renewal with 7 days remaining gives you time; a complete infrastructure failure during renewal does not.
Testing Your Renewal Pipeline
Use Let’s Encrypt’s staging environment to validate your entire pipeline without consuming rate limits:
certbot certonly \ --staging \ --dns-route53 \ -d "test.example.com" \ --dry-run
## Simulate failure scenariossystemctl stop nginx && certbot renew --dry-run || echo "HTTP-01 correctly failed"systemctl start nginxRun these tests monthly as part of your infrastructure validation. A renewal pipeline that worked six months ago may fail today due to dependency updates or infrastructure changes.
With failure handling in place, you now have a complete, production-ready certificate management system—from initial issuance through automated renewal to graceful degradation when things go wrong.
Key Takeaways
- Configure Certbot with systemd timers and post-renewal hooks to eliminate manual certificate renewal entirely
- Use DNS-01 challenges for wildcard certificates and internal services that lack public HTTP endpoints
- Implement certificate expiry monitoring with alerting at 30, 14, and 7 days before expiry as defense in depth
- Test your renewal pipeline monthly by forcing a dry-run renewal and verifying service reload hooks execute correctly
- Document and practice your emergency manual renewal procedure so it’s ready when automation fails