cert-manager in Production: TLS Automation for Kubernetes
Managing TLS certificates manually — tracking expiry, rotating secrets, updating Ingress configs — doesn't scale. cert-manager automates the full certificate lifecycle in Kubernetes: requesting, renewing, and distributing certificates from Let's Encrypt, a private CA, or any ACME-compatible provider. Here's how to use it reliably in production.

Expired TLS certificates cause production outages. The fix is rarely the rotation itself — it's the lack of automation that let the certificate expire unnoticed. cert-manager (CNCF graduated) solves this by treating TLS certificates as Kubernetes resources: you declare what certificate you want, cert-manager requests it from an issuer (Let's Encrypt, a private CA, Vault, etc.), stores the result in a Kubernetes Secret, and renews it automatically before it expires.
The operational model is declarative and GitOps-friendly: certificate requests, issuers, and renewal policy are YAML objects in your repository. No manual renewal runbooks, no certificate expiry cron jobs, no out-of-band PKI processes.
Core Concepts
cert-manager introduces four main CRDs:
| Resource | Scope | Purpose |
|---|---|---|
Issuer | Namespace | Issues certificates within one namespace |
ClusterIssuer | Cluster | Issues certificates across all namespaces |
Certificate | Namespace | Declares a certificate to request and keep renewed |
CertificateRequest | Namespace | One-time certificate signing request (managed by cert-manager) |
For most platforms, ClusterIssuer is the right choice — one issuer definition that all teams can reference, rather than per-namespace issuers that need to be replicated.
Installation
1helm repo add jetstack https://charts.jetstack.io
2helm repo update
3
4helm install cert-manager jetstack/cert-manager \
5 --namespace cert-manager \
6 --create-namespace \
7 --set crds.enabled=trueVerify the cert-manager webhook and controller are running:
1kubectl get pods -n cert-manager
2# cert-manager-xxxxx 1/1 Running 0 2m
3# cert-manager-webhook-xxxxx 1/1 Running 0 2m
4# cert-manager-cainjector-xxxxx 1/1 Running 0 2m
5
6# Verify the installation is working correctly
7kubectl cert-manager check apiImportant: cert-manager's webhook has failurePolicy: Fail and applies to cert-manager CRD operations. Ensure the cert-manager namespace is excluded from any other validating webhooks you run, to prevent circular webhook failures. See Kubernetes Admission Webhooks for webhook failure mode details.
Let's Encrypt: HTTP-01 Challenge
HTTP-01 validation is the simplest ACME method: cert-manager creates a temporary Ingress rule that serves the challenge token on http://<domain>/.well-known/acme-challenge/<token>. Let's Encrypt verifies it, then issues the certificate.
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4 name: letsencrypt-prod
5spec:
6 acme:
7 email: platform@example.com
8 server: https://acme-v02.api.letsencrypt.org/directory
9 privateKeySecretRef:
10 name: letsencrypt-prod-account-key # ACME account private key
11 solvers:
12 - http01:
13 ingress:
14 ingressClassName: nginxAlways test with the Let's Encrypt staging server first — it has no rate limits and issues untrusted certificates for testing:
# Staging issuer for testing (same structure, different server URL)
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directoryHTTP-01 limitations: Only validates a single domain (no wildcard certificates). The domain must be publicly resolvable during validation. Not suitable for internal services or wildcard certs.
Let's Encrypt: DNS-01 Challenge
DNS-01 validation adds a TXT record to _acme-challenge.<domain> to prove domain ownership. It supports wildcard certificates and works for internal services (the domain doesn't need to be publicly accessible, only DNS-resolvable).
Route 53
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4 name: letsencrypt-prod-dns
5spec:
6 acme:
7 email: platform@example.com
8 server: https://acme-v02.api.letsencrypt.org/directory
9 privateKeySecretRef:
10 name: letsencrypt-prod-dns-key
11 solvers:
12 - dns01:
13 route53:
14 region: us-east-1
15 hostedZoneID: Z1234567890ABC
16 # Use EKS Pod Identity or IRSA — don't store AWS keys in a Secret
17 # The cert-manager SA needs the IAM role annotation or PodIdentityAssociationIAM policy for cert-manager's Route 53 access:
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Effect": "Allow",
6 "Action": "route53:GetChange",
7 "Resource": "arn:aws:route53:::change/*"
8 },
9 {
10 "Effect": "Allow",
11 "Action": [
12 "route53:ChangeResourceRecordSets",
13 "route53:ListResourceRecordSets"
14 ],
15 "Resource": "arn:aws:route53:::hostedzone/Z1234567890ABC"
16 }
17 ]
18}1# Annotate cert-manager's ServiceAccount for IRSA or Pod Identity
2apiVersion: v1
3kind: ServiceAccount
4metadata:
5 name: cert-manager
6 namespace: cert-manager
7 annotations:
8 eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/cert-manager-dns01Cloudflare
1# Store Cloudflare API token in a Secret
2apiVersion: v1
3kind: Secret
4metadata:
5 name: cloudflare-api-token
6 namespace: cert-manager
7stringData:
8 api-token: "your-cloudflare-api-token"
9---
10spec:
11 acme:
12 solvers:
13 - dns01:
14 cloudflare:
15 apiTokenSecretRef:
16 name: cloudflare-api-token
17 key: api-tokenCertificate Resource
Certificate is the primary resource you interact with — it declares what you want, and cert-manager reconciles the actual certificate stored in a Kubernetes Secret:
1apiVersion: cert-manager.io/v1
2kind: Certificate
3metadata:
4 name: api-tls
5 namespace: production
6spec:
7 secretName: api-tls-secret # cert-manager writes the cert here
8 duration: 2160h # 90 days (Let's Encrypt maximum)
9 renewBefore: 720h # Renew 30 days before expiry
10 dnsNames:
11 - api.example.com
12 - "*.api.example.com" # Wildcard (requires DNS-01)
13 issuerRef:
14 name: letsencrypt-prod-dns
15 kind: ClusterIssuer
16 privateKey:
17 algorithm: ECDSA
18 size: 256 # P-256 — smaller and faster than RSA 2048
19 rotationPolicy: Always # Rotate private key on each renewalThe resulting Secret contains:
api-tls-secret:
tls.crt — certificate + full chain
tls.key — private key
ca.crt — issuer CA (populated for private issuers; empty for Let's Encrypt)
Check certificate status:
1kubectl get certificate -n production
2# NAME READY SECRET AGE
3# api-tls True api-tls-secret 2d
4
5kubectl describe certificate api-tls -n production
6# Status:
7# Conditions:
8# Type: Ready
9# Reason: Ready
10# Message: Certificate is up to date and has not expired
11# Not After: 2026-08-07T00:00:00Z
12# Not Before: 2026-05-09T00:00:00Z
13# Renewal Time: 2026-07-08T00:00:00Z # 30 days before expiryIngress Annotation (Auto-Provisioning)
For Ingress-based services, cert-manager can auto-provision a Certificate from an annotation — no separate Certificate resource needed:
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: api
5 namespace: production
6 annotations:
7 cert-manager.io/cluster-issuer: letsencrypt-prod-dns
8spec:
9 ingressClassName: nginx
10 tls:
11 - hosts:
12 - api.example.com
13 secretName: api-tls-secret # cert-manager creates and manages this Secret
14 rules:
15 - host: api.example.com
16 http:
17 paths:
18 - path: /
19 pathType: Prefix
20 backend:
21 service:
22 name: api
23 port:
24 number: 80cert-manager's ingress-shim watches Ingresses with this annotation and creates a corresponding Certificate object automatically. For Gateway API HTTPRoute resources, use the cert-manager.io/cluster-issuer annotation on the parent Gateway resource.
Private CA Hierarchy
For internal services (service-to-service mTLS, internal APIs) where Let's Encrypt is inappropriate, cert-manager can manage a private certificate authority:
1# Step 1: Self-signed root CA
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5 name: selfsigned-issuer
6spec:
7 selfSigned: {}
8---
9# Step 2: Generate the root CA certificate
10apiVersion: cert-manager.io/v1
11kind: Certificate
12metadata:
13 name: private-root-ca
14 namespace: cert-manager
15spec:
16 isCA: true
17 commonName: "My Org Internal Root CA"
18 secretName: private-root-ca-secret
19 duration: 87600h # 10 years for root CA
20 renewBefore: 8760h # Renew 1 year before expiry
21 privateKey:
22 algorithm: ECDSA
23 size: 384
24 issuerRef:
25 name: selfsigned-issuer
26 kind: ClusterIssuer
27---
28# Step 3: CA issuer backed by the root certificate
29apiVersion: cert-manager.io/v1
30kind: ClusterIssuer
31metadata:
32 name: private-ca-issuer
33spec:
34 ca:
35 secretName: private-root-ca-secretNow use private-ca-issuer in Certificate resources for any internal service:
1apiVersion: cert-manager.io/v1
2kind: Certificate
3metadata:
4 name: payments-service-tls
5 namespace: payments
6spec:
7 secretName: payments-service-tls-secret
8 duration: 720h # 30 days — short-lived for internal certs
9 renewBefore: 240h # Renew 10 days before expiry
10 dnsNames:
11 - payments.payments.svc.cluster.local
12 - payments.payments.svc
13 issuerRef:
14 name: private-ca-issuer
15 kind: ClusterIssuerFor a three-tier hierarchy (root CA → intermediate CA → leaf certs), create an intermediate CA Certificate (isCA: true) signed by the root CA issuer, then create another ClusterIssuer backed by the intermediate CA secret. Leaf certificates are signed by the intermediate — the root CA key is used only for intermediate CA issuance and can be kept offline.
trust-manager: Distributing CA Bundles
Services that need to trust your private CA must have the root CA certificate available. trust-manager (cert-manager sub-project) distributes CA bundles across namespaces as ConfigMaps:
helm install trust-manager jetstack/trust-manager \
--namespace cert-manager \
--wait1apiVersion: trust.cert-manager.io/v1alpha1
2kind: Bundle
3metadata:
4 name: private-ca-bundle
5spec:
6 sources:
7 - secret:
8 name: "private-root-ca-secret"
9 namespace: cert-manager # nested inside secret, not as sibling
10 key: "ca.crt"
11 target:
12 configMap:
13 key: "ca-bundle.crt"
14 namespaceSelector:
15 matchExpressions:
16 - key: kubernetes.io/metadata.name
17 operator: NotIn
18 values: [kube-system]trust-manager watches the source Secret and syncs the CA bundle to a ConfigMap named private-ca-bundle in every matching namespace. Applications mount this ConfigMap and use it as their CA trust store:
1# Application pod
2volumes:
3 - name: ca-bundle
4 configMap:
5 name: private-ca-bundle
6containers:
7 - name: app
8 volumeMounts:
9 - name: ca-bundle
10 mountPath: /etc/ssl/certs/internal-ca.crt
11 subPath: ca-bundle.crt
12 env:
13 - name: SSL_CERT_FILE
14 value: /etc/ssl/certs/internal-ca.crtMonitoring Certificate Health
cert-manager exposes Prometheus metrics:
1# Key metrics:
2# certmanager_certificate_expiration_timestamp_seconds — Unix timestamp of expiry per certificate
3# certmanager_certificate_ready_status — 1=Ready, 0=Not Ready
4# certmanager_http_acme_client_request_count — ACME API call volume
5
6# Alert: certificate expiring within 15 days
7- alert: CertificateExpiringSoon
8 expr: (certmanager_certificate_expiration_timestamp_seconds - time()) < 86400 * 15
9 for: 1h
10 labels:
11 severity: warning
12 annotations:
13 summary: "Certificate {{ $labels.name }} expires in {{ $value | humanizeDuration }}"
14 description: "Namespace: {{ $labels.namespace }}, Secret: {{ $labels.secret_name }}"
15
16# Alert: certificate not in Ready state
17- alert: CertificateNotReady
18 expr: certmanager_certificate_ready_status{condition="False"} == 1
19 for: 10m
20 labels:
21 severity: critical
22 annotations:
23 summary: "Certificate {{ $labels.name }} is not Ready"Check certificate status without Prometheus:
1# List all certificates and their expiry
2kubectl get certificates -A
3kubectl get certificates -A -o custom-columns=\
4 NAMESPACE:.metadata.namespace,\
5 NAME:.metadata.name,\
6 READY:.status.conditions[0].status,\
7 EXPIRY:'.status.notAfter'
8
9# Describe a specific certificate for detailed status
10kubectl describe certificate api-tls -n production
11
12# Check the underlying CertificateRequest if renewal is failing
13kubectl get certificaterequest -n production
14kubectl describe certificaterequest api-tls-xxxxx -n productionFrequently Asked Questions
What's the difference between duration and renewBefore?
duration is the requested certificate validity period (cert-manager asks the issuer for a cert valid for this long). renewBefore is how far in advance of expiry cert-manager starts the renewal. Let's Encrypt caps duration at 90 days regardless of what you request, so 2160h (90d) is the effective maximum for ACME certificates. For private CAs you control the duration. A good rule: set renewBefore to at least 1/3 of duration, giving cert-manager plenty of retry time if the renewal fails on the first attempt.
How do I force a certificate renewal?
1# Option 1: Use the cert-manager kubectl plugin (recommended)
2kubectl cert-manager renew api-tls -n production
3# Marks the Certificate for immediate renewal; cert-manager processes it asynchronously
4
5# Option 2: Delete the CertificateRequest — cert-manager creates a new one immediately
6kubectl get certificaterequest -n production
7kubectl delete certificaterequest api-tls-xxxxx -n productionBoth approaches trigger renewal without deleting the existing Secret — the current certificate remains valid while the new one is issued, preventing downtime.
cert-manager webhook is blocking pod creation in my cluster
cert-manager's webhook applies to cert-manager CRD operations (Certificate, Issuer, etc.), not pods. If you see cert-manager-related webhook errors blocking workloads, check:
- The cert-manager webhook pod is running
- The webhook's
caBundleis correctly injected by cert-manager's CA injector - Your cluster's
failurePolicyandnamespaceSelectorconfiguration
If cert-manager itself is uninstallable due to a broken webhook, delete the webhook configurations (ValidatingWebhookConfiguration cert-manager-webhook) temporarily to recover.
Can cert-manager issue certificates for non-Kubernetes workloads?
The csi-driver-spiffe and csi-driver cert-manager add-ons mount certificates directly into pod filesystems without Kubernetes Secrets as an intermediary. For non-Kubernetes workloads, use Vault's PKI secrets engine directly (cert-manager can issue from Vault but that's still Kubernetes-native). For VMs or external services, consider Vault PKI or an ACME client like certbot running alongside the service.
For cert-manager and ExternalDNS together — automating both TLS certificate issuance and DNS record creation for new services — see ExternalDNS: Automated DNS Management for Kubernetes Services. For GitOps management of cert-manager resources, see Flux CD: GitOps for Kubernetes. For how cert-manager TLS integrates with admission webhooks (including cert-manager's own webhook), see Kubernetes Admission Webhooks. For EKS-specific IAM setup for DNS-01 Route 53 validation, see EKS Networking Deep Dive. For a focused guide on cert-manager setup — Let's Encrypt ACME, wildcard certificates, and DNS-01 challenge on Route 53 — see cert-manager: Automated TLS Certificates for Kubernetes.
Automating TLS across a multi-cluster platform? Talk to us at Coding Protocols — we help platform teams set up cert-manager with private CA hierarchies and trust distribution that works across environments.


