cert-manager on Kubernetes — HTTPS that renews itself (2026)
Install cert-manager, set up ClusterIssuers, choose HTTP-01 vs DNS-01, and debug failures. Verified against cert-manager 1.21 and Let's Encrypt rate limits.
Renewing TLS certificates manually is one of those tasks that works fine until it doesn’t — a cert expires on a Sunday night, half the platform goes down, and the runbook says “renew via the vendor portal, then update the K8s Secret, then restart the pods.” cert-manager removes that entire class of incident. This guide covers install, ClusterIssuer setup, when to use HTTP-01 vs DNS-01, and the specific failure modes that trip most teams up.
What cert-manager does
cert-manager is a Kubernetes controller that requests, renews, and rotates TLS certificates automatically. It supports Let’s Encrypt (the most common source), HashiCorp Vault, CyberArk, private PKI, and other issuers. In the ACME/Let’s Encrypt flow:
- You declare a
Certificateresource (or anIngresswith an annotation) - cert-manager talks to the CA (Let’s Encrypt) to request the cert
- Proves domain ownership via a challenge (HTTP-01 or DNS-01)
- Stores the issued cert in a Kubernetes
Secret - Renews automatically before expiry (default: when 2/3 of the certificate’s lifetime has elapsed — e.g., 30 days before expiry on a 90-day Let’s Encrypt cert)
Zero manual steps after the initial setup.
Install cert-manager (Helm, current version)
As of September 2026 the current release is cert-manager 1.21.2 (support window: 4 months per release, N+1 supported per cert-manager release policy — always at least two supported versions in flight).
Compatibility: Kubernetes 1.33 through 1.36. Older or newer clusters may need a different cert-manager version.
Install via Helm:
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.21.2 \
--set crds.enabled=true
crds.enabled=true installs the Custom Resource Definitions (Certificate, Issuer, ClusterIssuer, Order, Challenge, etc.) as part of the chart. Older docs sometimes recommend installing CRDs separately with a kubectl apply -f https://.../cert-manager.crds.yaml step — that’s fine but no longer necessary with the crds.enabled flag.
Verify install:
kubectl get pods -n cert-manager
Should show three Running pods: cert-manager, cert-manager-cainjector, cert-manager-webhook.
ClusterIssuer — always start with staging
ClusterIssuer is a cluster-scoped issuer any namespace can use. Issuer is the same thing scoped to one namespace. Most teams use ClusterIssuer exclusively — one issuer for the whole cluster, referenced from any workload.
Set up two ClusterIssuers: staging and production. This is the single most important lesson buried in cert-manager’s docs — Let’s Encrypt’s production limits are aggressive. Test everything against staging first.
Staging ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: YOUR_EMAIL@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Production ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: YOUR_EMAIL@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Same structure, different URL. Apply both. Point your first Ingress at letsencrypt-staging, verify the certificate issues cleanly, then switch the annotation to letsencrypt-prod.
HTTP-01 vs DNS-01 — pick the right solver
Let’s Encrypt needs to verify you control the domain. Two ways to prove it:
HTTP-01 (easier, most common)
cert-manager creates a temporary Ingress route serving a challenge file at /.well-known/acme-challenge/<token>. Let’s Encrypt hits your public URL, sees the token, marks the domain verified.
Requirements:
- Public HTTP (port 80) reachable from Let’s Encrypt’s servers
- An Ingress controller (nginx-ingress, Traefik, Contour) that cert-manager can inject a route into
Use when: simple domains, no wildcards, cluster is publicly reachable on port 80.
Fails when: your cluster is behind a firewall, on private IPs, or IPv6-only.
DNS-01 (required for wildcards, works everywhere)
cert-manager writes a temporary TXT record to your DNS zone. Let’s Encrypt queries the TXT record via DNS. No public HTTP needed.
Requirements:
- API access to your DNS provider (Cloudflare, Route53, Google Cloud DNS, etc.)
- API token/credentials scoped to only the specific zone
Use when:
- You need a wildcard cert (
*.YOUR_DOMAIN.com) — DNS-01 is the only way - Cluster isn’t publicly reachable on port 80
- You want to issue certs for internal services
Example — Cloudflare DNS-01 solver:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: YOUR_EMAIL@example.com
privateKeySecretRef:
name: letsencrypt-prod-dns-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "YOUR_DOMAIN.com"
Security note: the Cloudflare API token should have Zone.DNS edit permissions ONLY for the specific zone — not global account access. Store it in a Kubernetes Secret created outside git (Sealed Secrets, External Secrets Operator, or kubectl create secret from a local file that’s .gitignored).
Dual-solver ClusterIssuer
For clusters that serve some public domains via HTTP-01 and some internal zones via DNS-01, one ClusterIssuer can hold both solvers with a selector block on each. cert-manager picks the right solver based on the domain in the Certificate request.
First cert — the walkthrough
Two ways to request a cert. Pick one.
Option A — Ingress-driven (recommended for most sites)
Annotate your Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
namespace: default
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-staging"
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.YOUR_DOMAIN.com
secretName: myapp-tls
rules:
- host: myapp.YOUR_DOMAIN.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
Apply. cert-manager sees the annotation + tls block, creates a Certificate behind the scenes, runs the challenge, stores the cert in myapp-tls. Total time: 30-90 seconds for HTTP-01 in the happy path.
Option B — Explicit Certificate resource
For more control (custom durations, DNS-01, wildcards):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: myapp-cert
namespace: default
spec:
secretName: myapp-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- myapp.YOUR_DOMAIN.com
- api.myapp.YOUR_DOMAIN.com
duration: 2160h # 90 days (Let's Encrypt max)
renewBefore: 720h # renew 30 days before expiry
When it fails — the debug chain
cert-manager creates a chain of resources for every certificate request:
Certificate -> CertificateRequest -> Order -> Challenge
The failure is always at one specific point. Start at the top and work down — the first resource with an error is where the actual problem lives.
Debug commands (run in order)
# 1. Is the Certificate ready?
kubectl describe certificate myapp-cert -n default
Look at the Status.Conditions and Events sections. If Ready: True, done. If not, note the error.
# 2. What CertificateRequest was created?
kubectl get certificaterequest -n default
kubectl describe certificaterequest <name> -n default
Shows request-level errors (e.g., ClusterIssuer misconfigured).
# 3. What Order was created?
kubectl get order -n default
kubectl describe order <name> -n default
Shows the ACME order state (pending, valid, invalid).
# 4. What Challenge is failing?
kubectl get challenge -n default
kubectl describe challenge <name> -n default
Shows the actual challenge attempt — HTTP-01 GET result, DNS-01 TXT lookup result.
Ninety percent of the time, the Challenge output tells you exactly what’s wrong.
Common failure modes
Failure 1 — HTTP-01 challenge unreachable
Symptom: Challenge is pending forever. Describe shows something like Waiting for HTTP-01 challenge propagation: failed to perform self check GET request 'http://myapp.YOUR_DOMAIN.com/.well-known/acme-challenge/xyz': Get "...": context deadline exceeded.
Root cause: Let’s Encrypt (or cert-manager’s own self-check) can’t reach your Ingress on port 80. Common reasons:
- DNS not resolving to your Ingress IP yet — wait for propagation
- Cloud provider firewall blocking port 80 (allow HTTP inbound)
- Ingress controller only listening on HTTPS (port 443) — HTTP-01 needs port 80 open
- Ingress rewrites
/.well-known/*to something else — exclude the ACME path
Fix: ensure port 80 is publicly reachable and the Ingress passes /.well-known/acme-challenge/* unmodified to cert-manager’s temporary solver pod.
Failure 2 — DNS-01 propagation timeout
Symptom: Challenge stays pending, describe shows waiting for DNS-01 challenge propagation.
Root cause: the TXT record was created but Let’s Encrypt’s DNS resolvers haven’t picked it up yet.
Fix: patience — DNS-01 typically takes 30-120 seconds; some providers up to 5 minutes. If it persists:
- Verify the TXT record actually exists:
dig _acme-challenge.YOUR_DOMAIN.com TXT - Check the DNS provider’s API credentials are correct
- Increase
--dns01-recursive-nameservers-onlyand--dns01-recursive-nameserversflags on the cert-manager Deployment if your provider has slow propagation
Failure 3 — Let’s Encrypt rate limits hit
Symptom: Order fails with too many certificates already issued for this exact set of domains.
Root cause: Let’s Encrypt’s production rate limits are strict:
- 50 certificates per registered domain per week (refills at 1 cert every ~202 minutes)
- 5 duplicate certificates per exact name set per week (global across all accounts)
- Up to 1,152 consecutive authorization failures per identifier (refills at 1 per identifier per day; resets to zero on next successful auth)
A misconfigured cluster in a loop can burn through 50 in an hour.
Fix:
- Immediately switch the Issuer annotation on the affected Ingress to
letsencrypt-stagingto stop consuming production quota - Wait for the weekly rate limit window to reset
- Debug the underlying issue on staging (staging’s limits are effectively unlimited: 30,000 new certs per second)
- Once fixed on staging, switch back to production for the final good cert
Prevention: ALWAYS start with staging. Move to production only after a clean pass on staging.
Failure 4 — subPath Secret mounts miss rotated certs
Symptom: Certificate renews successfully in the Secret, but pods keep serving the expired cert.
Root cause: pods that mount the TLS Secret using subPath don’t see updates to the underlying Secret after rotation — Kubernetes doesn’t propagate Secret changes into subPath mounts.
Fix:
- Mount the Secret WITHOUT
subPathif possible - OR use Reloader (github.com/stakater/Reloader) or Trust Manager to trigger pod rollouts on Secret changes
- OR restart pods on a schedule (every 60 days is safe for 90-day certs)
This is one of the silent-failure classes that only shows up 60-89 days after initial success.
Failure 5 — Shared ACME account key across clusters
Symptom: Sporadic Order failures with order already exists or race conditions when multiple clusters use the same issuer email + account key.
Root cause: two cert-manager instances (dev cluster + prod cluster) using the same privateKeySecretRef name for the same issuer email conflict at the ACME layer.
Fix: use different privateKeySecretRef names per cluster (e.g., letsencrypt-prod-account-key-dev vs letsencrypt-prod-account-key-prod). Each gets its own ACME account.
Wildcard certificates
Only DNS-01 can issue wildcards. HTTP-01 cannot prove ownership of subdomains you haven’t provisioned yet.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: default
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-prod-dns
kind: ClusterIssuer
dnsNames:
- "*.YOUR_DOMAIN.com"
- "YOUR_DOMAIN.com" # apex, since wildcard doesn't cover it
Notice *.YOUR_DOMAIN.com doesn’t include YOUR_DOMAIN.com (the apex). Include both explicitly if you need the apex covered.
Production defaults worth setting
Before you consider cert-manager “done”:
- Prometheus metrics enabled — cert-manager exposes
/metricson port 9402. Alert oncertmanager_certificate_expiration_timestamp_seconds - time() < 21*24*60*60(21 days) to catch renewal failures early. - CertificateRequest cleanup — the
--enable-certificate-owner-ref=trueflag on the cert-manager Deployment garbage-collects orphan CertificateRequests when a Certificate is deleted. - Auto-approval limits — for large clusters, tune
--concurrent-workersto avoid overwhelming the ACME API. - Backup the Let’s Encrypt account private key — the Secret referenced by
privateKeySecretRef. Losing it means creating a new ACME account and losing your rate limit history. - Trust Manager for CA distribution — if you also serve certificates from a private CA, use Trust Manager (a cert-manager sub-project) to distribute the CA bundle to workloads.
Bottom line
cert-manager removes an entire category of on-call incident: TLS certs that silently expire. Install it once via Helm, create two ClusterIssuers (staging + production), point Ingresses at staging first with a cert-manager.io/cluster-issuer annotation, verify the challenge passes, then flip to production. HTTP-01 works for most cases; use DNS-01 for wildcards or when port 80 isn’t reachable. When something fails, walk the Certificate → CertificateRequest → Order → Challenge chain — the first resource with an error tells you exactly what’s wrong. Set up Prometheus alerts on impending expiry so a rotation that silently breaks doesn’t become a Sunday-night outage.
DevOps YAML Pack
40+ production-ready configs — Kubernetes, Docker, Terraform, Ansible, Helm, GitHub Actions. Every file commented. Copy, edit two lines, ship. MIT license, no watermarks.
Get the pack — ₹499 →