Kubernetes
13 min readMay 7, 2026

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.

CO
Coding Protocols Team
Platform Engineering
cert-manager in Production: TLS Automation for Kubernetes

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:

ResourceScopePurpose
IssuerNamespaceIssues certificates within one namespace
ClusterIssuerClusterIssues certificates across all namespaces
CertificateNamespaceDeclares a certificate to request and keep renewed
CertificateRequestNamespaceOne-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

bash
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=true

Verify the cert-manager webhook and controller are running:

bash
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 api

Important: 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.

yaml
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: nginx

Always test with the Let's Encrypt staging server first — it has no rate limits and issues untrusted certificates for testing:

yaml
# Staging issuer for testing (same structure, different server URL)
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory

HTTP-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

yaml
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 PodIdentityAssociation

IAM policy for cert-manager's Route 53 access:

json
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}
yaml
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-dns01

Cloudflare

yaml
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-token

Certificate 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:

yaml
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 renewal

The 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:

bash
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 expiry

Ingress Annotation (Auto-Provisioning)

For Ingress-based services, cert-manager can auto-provision a Certificate from an annotation — no separate Certificate resource needed:

yaml
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: 80

cert-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:

yaml
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-secret

Now use private-ca-issuer in Certificate resources for any internal service:

yaml
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: ClusterIssuer

For 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:

bash
helm install trust-manager jetstack/trust-manager \
  --namespace cert-manager \
  --wait
yaml
1apiVersion: 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:

yaml
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.crt

Monitoring Certificate Health

cert-manager exposes Prometheus metrics:

yaml
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:

bash
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 production

Frequently 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?

bash
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 production

Both 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:

  1. The cert-manager webhook pod is running
  2. The webhook's caBundle is correctly injected by cert-manager's CA injector
  3. Your cluster's failurePolicy and namespaceSelector configuration

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.

Related Topics

cert-manager
TLS
Kubernetes
Let's Encrypt
PKI
CNCF
Security
Platform Engineering

Read Next