Skip to main content
Security·6 min read

Managing TLS Certificates Across 100+ Domains Without Expiration Outages

Automate TLS certificate lifecycle management with cert-manager, Let's Encrypt, and monitoring. Prevent SSL expiration outages with automated renewal, alerting, and multi-domain strategies.

DT

DevOps Engineer & Technical Writer

TL;DR — Quick Fix

# Check expiring certificates across all domains

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \

openssl x509 -noout -dates

# Install cert-manager for automated renewal

helm install cert-manager jetstack/cert-manager \

--namespace cert-manager --create-namespace \

--set installCRDs=true

---

Why Certificates Expire in Production

CERTIFICATE EXPIRATION — IMPACT TIMELINE Day 1-60 Certificate valid Everything works Day 60-85 Renewal window Should auto-renew here Day 90 (Expiry) SITE DOWN ERR_CERT_DATE_INVALID WHY MANUAL RENEWAL FAILS: Calendar reminders get snoozed. Team members leave. Domains get forgotten. The only reliable solution is full automation with monitoring.

Certificate expiration is one of the most common causes of production outages and one of the most preventable.

Cert-Manager: Automated Certificate Lifecycle

Installation

helm repo add jetstack https://charts.jetstack.io

helm repo update

helm install cert-manager jetstack/cert-manager \

--namespace cert-manager \

--create-namespace \

--version v1.15.0 \

--set installCRDs=true \

--set prometheus.enabled=true

ClusterIssuer for Let's Encrypt

apiVersion: cert-manager.io/v1

kind: ClusterIssuer

metadata:

name: letsencrypt-prod

spec:

acme:

server: https://acme-v02.api.letsencrypt.org/directory

email: devops@yourcompany.com

privateKeySecretRef:

name: letsencrypt-prod-account-key

solvers:

- http01:

ingress:

class: nginx

- dns01:

route53:

region: us-east-1

hostedZoneID: Z1234567890

selector:

dnsZones:

- "yourcompany.com"

Certificate Resource

apiVersion: cert-manager.io/v1

kind: Certificate

metadata:

name: api-tls

namespace: production

spec:

secretName: api-tls-secret

issuerRef:

name: letsencrypt-prod

kind: ClusterIssuer

dnsNames:

- api.yourcompany.com

- api-v2.yourcompany.com

renewBefore: 720h

duration: 2160h

Wildcard Certificate

apiVersion: cert-manager.io/v1

kind: Certificate

metadata:

name: wildcard-tls

namespace: production

spec:

secretName: wildcard-tls-secret

issuerRef:

name: letsencrypt-prod

kind: ClusterIssuer

dnsNames:

- "*.yourcompany.com"

- "yourcompany.com"

Ingress Annotation (Simplest Method)

apiVersion: networking.k8s.io/v1

kind: Ingress

metadata:

name: api-ingress

annotations:

cert-manager.io/cluster-issuer: letsencrypt-prod

spec:

tls:

- hosts:

- api.yourcompany.com

secretName: api-tls-auto

rules:

- host: api.yourcompany.com

http:

paths:

- path: /

pathType: Prefix

backend:

service:

name: api-service

port:

number: 80

Monitoring Certificate Expiry

Prometheus Alerts

groups:

- name: certificate-alerts

rules:

- alert: CertificateExpiringSoon

expr: certmanager_certificate_expiration_timestamp_seconds - time() < 2592000

for: 1h

labels:

severity: warning

annotations:

summary: "Certificate {{ $labels.name }} expires in less than 30 days"

- alert: CertificateExpiringCritical

expr: certmanager_certificate_expiration_timestamp_seconds - time() < 604800

for: 10m

labels:

severity: critical

annotations:

summary: "Certificate {{ $labels.name }} expires in less than 7 days!"

- alert: CertificateRenewalFailed

expr: certmanager_certificate_ready_status{condition="False"} == 1

for: 30m

labels:

severity: critical

annotations:

summary: "Certificate {{ $labels.name }} is not ready — renewal may have failed"

External Certificate Monitoring Script

#!/bin/bash

# check-certs.sh — Run daily via cron

DOMAINS=(

"api.yourcompany.com"

"app.yourcompany.com"

"admin.yourcompany.com"

"cdn.yourcompany.com"

)

WARN_DAYS=30

CRITICAL_DAYS=7

for domain in "${DOMAINS[@]}"; do

EXPIRY=$(echo | openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | \

openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

if [ -z "$EXPIRY" ]; then

echo "ERROR: Cannot check $domain"

continue

fi

EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)

NOW_EPOCH=$(date +%s)

DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_LEFT" -lt "$CRITICAL_DAYS" ]; then

echo "CRITICAL: $domain expires in $DAYS_LEFT days ($EXPIRY)"

elif [ "$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

AWS Certificate Manager (ACM)

resource "aws_acm_certificate" "api" {

domain_name = "api.yourcompany.com"

subject_alternative_names = ["api-v2.yourcompany.com"]

validation_method = "DNS"

lifecycle {

create_before_destroy = true

}

}

resource "aws_route53_record" "cert_validation" {

for_each = {

for dvo in aws_acm_certificate.api.domain_validation_options : dvo.domain_name => {

name = dvo.resource_record_name

record = dvo.resource_record_value

type = dvo.resource_record_type

}

}

zone_id = data.aws_route53_zone.main.zone_id

name = each.value.name

type = each.value.type

records = [each.value.record]

ttl = 60

}

resource "aws_acm_certificate_validation" "api" {

certificate_arn = aws_acm_certificate.api.arn

validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]

}

ACM certificates auto-renew as long as DNS validation records remain in place.

Troubleshooting Cert-Manager

Certificate Stuck in "Not Ready"

# Check certificate status

kubectl get certificates -A

kubectl describe certificate <name> -n <namespace>

# Check ACME orders and challenges

kubectl get orders -A

kubectl get challenges -A

kubectl describe challenge <name> -n <namespace>

Common Failures

ErrorCauseFix
<code class="inline-code">Waiting for DNS propagation</code>DNS record not visibleCheck Route53 permissions, wait for TTL
<code class="inline-code">Connection refused</code> on HTTP-01Port 80 blockedOpen port 80 for ACME validation
<code class="inline-code">Too many certificates</code>Let's Encrypt rate limitUse staging for testing, consolidate SANs
<code class="inline-code">Certificate not ready</code>Issuer misconfiguredCheck ClusterIssuer logs and credentials

---

Frequently Asked Questions

How does cert-manager handle renewal automatically?

Cert-manager checks the renewBefore field (default: 2/3 of certificate lifetime). For a 90-day Let's Encrypt cert, it renews at 60 days. It re-solves the ACME challenge, gets a new cert, and updates the Kubernetes Secret automatically.

Can I use cert-manager with non-Kubernetes services?

Yes, cert-manager stores certificates as Kubernetes Secrets. Sync these to external systems using external-secrets-operator or CSI secret store drivers. For non-Kubernetes infrastructure, use Terraform's ACME provider.

What happens if Let's Encrypt is down during renewal?

Cert-manager retries with exponential backoff. Since renewal starts 30 days before expiry, temporary CA outages won't cause issues. The certificate remains valid until its actual expiry date.

Should I use wildcard certificates or individual certs?

Wildcards are simpler (one cert for all subdomains) but have a larger blast radius if compromised. Use wildcards for internal services and individual certs for public-facing critical services.

How do I handle certificates for internal services?

Use a private CA (cert-manager self-signed or CA issuer) for internal services. This avoids rate limits and doesn't require public DNS validation. Distribute the CA certificate to all services that need to trust internal TLS.

---