
Somewhere in the next 90 days, an expired TLS certificate is going to take down a small business’s website, API, or email in the middle of the night. It’s not a sophisticated attack. It’s not a cloud outage. It’s a certificate that reached its end date while nobody was watching — and browsers, mobile apps, and payment integrations all refuse to talk to a server they can’t trust.
In 2026, certificates are shorter-lived than ever (Let’s Encrypt pushed the industry to 90-day certificates, and shorter lifespans keep coming). That means the manual “renew it when I remember” approach fails four times more often than it used to — and each failure is an outage. The fix is boring, proven automation: cert-manager on Kubernetes, with monitoring that proves renewal is actually working. Here’s the complete setup for a lean SMB team.
Why certificates still expire in 2026
Renewal failures almost never happen because automation is missing. They happen because automation runs silently and nobody verifies it. Common failure patterns we see in SMB audits:
- A cron job that renews certificates but logs to a file nobody reads — the cert fails validation in week two and expires in week thirteen.
- Wildcard certificates generated once, copied to three servers, and renewed on none of them.
- Let’s Encrypt’s rate limits tripping on a misconfigured testing loop, blocking the real renewal.
- DNS-01 challenges failing because the DNS provider’s API token expired — and the challenge retry logic silently gives up.
The business impact is outsized: one expired cert can take down your website, your customer portal, and your email authentication (DKIM/SPF don’t help when the TLS handshake itself fails). The good news: on Kubernetes, cert-manager turns issuance and renewal into declarative, observable infrastructure. If your DNS is still managed by hand-edited panels, our DNS-as-code guide is the foundation this build assumes.
Install cert-manager and create a staging ClusterIssuer
Install cert-manager with Helm (you’ll need its CRDs):
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
Then define a ClusterIssuer so every namespace in the cluster can use the same issuance policy. Always start with Let’s Encrypt’s staging environment — it has much friendlier rate limits for testing:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
class: nginx
kubectl apply -f clusterissuer-staging.yaml
kubectl get clusterissuer letsencrypt-staging -o wide
A READY=True status means ACME is reachable and the account is registered. Point one test Ingress at it with the annotation cert-manager.io/cluster-issuer: letsencrypt-staging, confirm the certificate issues, and then swap the issuer to letsencrypt-prod. Never skip the staging round — a failed request against production rate limits can block a real renewal for a week.
Issue and renew automatically: HTTP-01 vs DNS-01
For plain web services, cert-manager solves HTTP-01 challenges automatically by injecting a token into your Ingress — zero extra config per host:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop-web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.yourdomain.com"]
secretName: shop-web-tls
rules:
- host: shop.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shop-web
port:
number: 80
For wildcards or services that must stay reachable during issuance (APIs hit by third parties), use a Certificate resource with the DNS-01 solver instead. Here’s a Cloudflare wildcard example — the API token goes in a Secret, which you should source from your secrets manager rather than committing it (see our secrets management guide):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-yourdomain
namespace: platform
spec:
secretName: wildcard-yourdomain-tls
dnsNames:
- "*.yourdomain.com"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
privateKey:
algorithm: ECDSA
size: 384
cert-manager tracks the certificate lifetime and renews automatically at two-thirds of the validity period — for a 90-day certificate, that’s 30 days of slack. It also retries with exponential backoff, so transient ACME failures don’t become expiration events. An ECDSA key (instead of RSA) keeps handshakes fast for mobile clients.
Prove renewal works — monitoring that catches failures first
If you can’t prove a renewal happened, assume it didn’t. Three cheap checks give you 95% of the safety:
1. Expose cert-manager’s metrics. The Prometheus metrics endpoint includes certmanager_certificate_expiration_timestamp_seconds per certificate. Alert when any cert has less than 21 days left or when a Certificate object reports Ready=False:
groups:
- name: certs
rules:
- alert: CertificateExpiringSoon
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 21 * 86400
for: 30m
labels:
severity: warning
2. Check from the outside, where users sit. A cron job that hits each public endpoint and fails if the served cert expires soon:
#!/usr/bin/env bash
# check-tls.sh — alert if any host's cert expires within 21 days
for host in www.yourdomain.com api.yourdomain.com mail.yourdomain.com; do
if openssl s_client -servername "$host" -connect "$host":443 \
</dev/null 2>/dev/null | openssl x509 -noout -checkend 1814400; then
echo "OK: $host safe for 21+ days"
else
echo "ALERT: $host expires within 21 days"; exit 1
fi
done
Wire the exit code into your monitoring (Uptime Kuma, Grafana, or even a cron notification). checkend 1814400 is 21 days in seconds — fail, loudly, with three weeks of runway, not three days.
3. Add a renewal dry-run to your pipeline. The certbot renew --dry-run equivalent in cert-manager land is simply recreating the Certificate in a test namespace against the staging issuer during CI — the same discipline as testing any other deployment change (and the exact lesson from our safe deployment pipeline guide).
When renewal fails: a practical troubleshooting path
The three failures that actually happen, and how to confirm them in minutes:
- Rate limited by Let’s Encrypt:
kubectl describe certificateshows the ACME error;acme-v02.api.letsencrypt.orgreturns 429. Fix: use the staging issuer for tests, and wait out the window (failures reset after a week). - DNS-01 can’t find the TXT record: the error “no matching TXT record” almost always means propagation delay or a typo in the zone. Check with
dig TXT _acme-challenge.yourdomain.comand confirm your DNS provider zone actually matches the domain in the Certificate. - HTTP-01 blocked by a firewall: the challenge requires port 80 to be reachable from the internet. If your cluster sits behind a whitelist, use DNS-01 instead — it needs no inbound traffic at all.
Automated certificate management is one of the highest-ROI reliability investments an SMB can make: a few YAML files eliminate a whole class of production incidents permanently. But it’s only trustworthy if the renewal loop is verified end-to-end — which is exactly what the checks above give you.
Not sure your current certificate setup will survive the next 90 days? Book a free 30-minute DevOps consulting session — we’ll audit your TLS, DNS, and deployment pipelines and hand you a prioritized fix list.