Kubernetes
11 min readMay 7, 2026

cert-manager: Automated TLS for Kubernetes

cert-manager automates the full TLS certificate lifecycle in Kubernetes — issuance, renewal, and rotation — using Let's Encrypt (ACME) or private CAs. It watches Certificate resources, solves ACME challenges, stores certificates as Kubernetes Secrets, and renews them before expiry. This covers the production setup: Cluster Issuer with DNS-01 challenge on Route 53, certificate for wildcard domains, Ingress integration, and the operational patterns that prevent certificate-related outages.

CO
Coding Protocols Team
Platform Engineering
cert-manager: Automated TLS for Kubernetes

Every Kubernetes cluster that serves HTTPS needs TLS certificates. Manual certificate management at scale is unsustainable — cert-manager automates the entire lifecycle. It watches Certificate resources, solves ACME HTTP-01 or DNS-01 challenges, stores the resulting cert and key as Kubernetes Secrets, and renews them automatically before they expire.

The Let's Encrypt free tier covers most use cases. For internal services on private networks (where Let's Encrypt can't reach), cert-manager also integrates with Vault PKI, AWS Private CA, and self-signed CA chains.


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  --version v1.17.1 \
8  --set crds.enabled=true

The crds.enabled=true value installs CRDs as part of the Helm release — they'll be upgraded when cert-manager is upgraded. (The alternative is kubectl apply -f https://... to install CRDs separately before the chart.)


ClusterIssuer: Let's Encrypt with DNS-01

For wildcard certificates and clusters not reachable from the internet, DNS-01 is the right challenge type. cert-manager creates a TXT record in Route 53 to prove domain ownership.

IAM Role for Route 53

cert-manager needs permission to manage Route 53 records in the hosted zone:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Action": [
7        "route53:GetChange",
8        "route53:ChangeResourceRecordSets",
9        "route53:ListResourceRecordSets"
10      ],
11      "Resource": [
12        "arn:aws:route53:::hostedzone/Z1234567890ABC",
13        "arn:aws:route53:::change/*"
14      ]
15    },
16    {
17      "Effect": "Allow",
18      "Action": "route53:ListHostedZonesByName",
19      "Resource": "*"
20    }
21  ]
22}

Annotate cert-manager's ServiceAccount with IRSA:

bash
kubectl annotate serviceaccount cert-manager \
  --namespace cert-manager \
  eks.amazonaws.com/role-arn=arn:aws:iam::123456789:role/cert-manager-route53

ClusterIssuer

yaml
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4  name: letsencrypt-prod
5spec:
6  acme:
7    server: https://acme-v02.api.letsencrypt.org/directory
8    email: platform@codingprotocols.com
9    privateKeySecretRef:
10      name: letsencrypt-prod-account-key    # Stored in cert-manager namespace
11    solvers:
12      - dns01:
13          route53:
14            region: us-east-1
15            hostedZoneID: Z1234567890ABC

For staging (testing — avoid Let's Encrypt rate limits during setup):

yaml
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4  name: letsencrypt-staging
5spec:
6  acme:
7    server: https://acme-staging-v02.api.letsencrypt.org/directory
8    email: platform@codingprotocols.com
9    privateKeySecretRef:
10      name: letsencrypt-staging-account-key
11    solvers:
12      - dns01:
13          route53:
14            region: us-east-1
15            hostedZoneID: Z1234567890ABC

Always test with letsencrypt-staging first. The staging CA is deliberately not trusted by browsers — staging certs will produce a TLS error for real users. That's intentional: staging lets you verify cert-manager's configuration and ACME challenge flow without burning production rate limit quota (5 duplicate certs per domain per week).


Certificate Resource

Create certificates explicitly (recommended for non-Ingress consumers or wildcard certs):

yaml
1apiVersion: cert-manager.io/v1
2kind: Certificate
3metadata:
4  name: codingprotocols-wildcard
5  namespace: ingress-nginx    # Must be in the namespace where the consuming Ingress/Service lives
6spec:
7  secretName: codingprotocols-wildcard-tls    # cert-manager creates this Secret
8
9  issuerRef:
10    name: letsencrypt-prod
11    kind: ClusterIssuer    # ClusterIssuer is cluster-scoped; Issuer is namespace-scoped
12
13  commonName: "*.codingprotocols.com"
14  dnsNames:
15    - "*.codingprotocols.com"
16    - "codingprotocols.com"          # Include apex domain separately
17
18  # Renewal: cert-manager renews at renewBefore before expiry (default 30 days)
19  # Let's Encrypt certs are valid for 90 days; renewal at 60 days
20  duration: 2160h      # 90 days (default)
21  renewBefore: 720h    # Renew when 30 days remain (default)

Check certificate status:

bash
1kubectl get certificate -n ingress-nginx
2# NAME                        READY   SECRET                          AGE
3# codingprotocols-wildcard    True    codingprotocols-wildcard-tls    5d
4
5kubectl describe certificate codingprotocols-wildcard -n ingress-nginx
6# Events:
7#   Normal  Issuing  cert-manager has started issuing a certificate
8#   Normal  Issued   Certificate issued successfully

Ingress Integration

For Ingress-based TLS, annotate the Ingress with the ClusterIssuer. cert-manager watches for the annotation and creates a Certificate resource automatically:

yaml
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4  name: payments-api
5  namespace: payments
6  annotations:
7    cert-manager.io/cluster-issuer: "letsencrypt-prod"    # Triggers cert-manager
8spec:
9  ingressClassName: nginx
10  tls:
11    - hosts:
12        - api.codingprotocols.com
13      secretName: api-tls    # cert-manager creates this Secret
14  rules:
15    - host: api.codingprotocols.com
16      http:
17        paths:
18          - path: /
19            pathType: Prefix
20            backend:
21              service:
22                name: payments-api
23                port:
24                  number: 8080

When cert-manager sees this Ingress, it creates a Certificate resource for api.codingprotocols.com, solves the DNS-01 challenge, and populates the api-tls Secret. The Ingress controller picks up the Secret once it's ready.


Gateway API Integration (2026 Standard)

In 2026, the Gateway API has replaced Ingress for modern routing. cert-manager integrates directly with the Gateway resource:

yaml
1apiVersion: gateway.networking.k8s.io/v1
2kind: Gateway
3metadata:
4  name: production
5  namespace: infra
6  annotations:
7    cert-manager.io/issuer: "letsencrypt-prod"
8spec:
9  gatewayClassName: nginx
10  listeners:
11    - name: https
12      protocol: HTTPS
13      port: 443
14      tls:
15        mode: Terminate
16        certificateRefs:
17          - kind: Secret
18            name: wildcard-tls

By annotating the Gateway, cert-manager automatically manages the certificate for all domains specified in the HTTPRoute objects attached to that listener. This is the cleaner, role-oriented way to handle TLS in modern clusters.


HTTP-01 Challenge (for non-wildcard certs without DNS access)

If your cluster is internet-reachable and you don't need wildcards, HTTP-01 is simpler — no DNS permissions required:

yaml
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4  name: letsencrypt-http01
5spec:
6  acme:
7    server: https://acme-v02.api.letsencrypt.org/directory
8    email: platform@codingprotocols.com
9    privateKeySecretRef:
10      name: letsencrypt-http01-account-key
11    solvers:
12      - http01:
13          ingress:
14            ingressClassName: nginx    # cert-manager creates a temporary Ingress for the challenge

HTTP-01 limitations: no wildcards, and the cluster must be reachable on port 80 from Let's Encrypt's validation servers. For clusters behind a VPN or private load balancer, use DNS-01.


Private CA with Vault PKI

For internal services that need TLS but shouldn't use Let's Encrypt (air-gapped environments, internal mTLS):

yaml
1apiVersion: cert-manager.io/v1
2kind: ClusterIssuer
3metadata:
4  name: vault-pki
5spec:
6  vault:
7    server: https://vault.internal:8200
8    path: pki/sign/kubernetes-role    # Vault PKI sign endpoint
9    auth:
10      kubernetes:
11        mountPath: /v1/auth/kubernetes
12        role: cert-manager
13        # No secretRef here — cert-manager uses its own ServiceAccount JWT to authenticate with Vault
14        # For static token auth, use vault.auth.tokenSecretRef instead of vault.auth.kubernetes

Vault PKI signs certificates with your internal CA. The internal CA's root cert needs to be distributed to clients for trust — it won't be trusted by default like Let's Encrypt.


Monitoring Certificate Expiry

cert-manager exports Prometheus metrics. Key alerts:

yaml
1# PrometheusRule for certificate expiry
2apiVersion: monitoring.coreos.com/v1
3kind: PrometheusRule
4metadata:
5  name: cert-manager-alerts
6  namespace: cert-manager
7spec:
8  groups:
9    - name: cert-manager
10      rules:
11        - alert: CertificateExpiringIn7Days
12          expr: |
13            certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600
14          for: 1h
15          labels:
16            severity: warning
17          annotations:
18            summary: "Certificate {{ $labels.name }} in {{ $labels.namespace }} expires in < 7 days"
19
20        - alert: CertificateNotReady
21          expr: certmanager_certificate_ready_status{condition="False"} == 1
22          for: 10m
23          labels:
24            severity: critical
25          annotations:
26            summary: "Certificate {{ $labels.name }} in {{ $labels.namespace }} is not ready"
promql
1# All certificates with their expiry times
2certmanager_certificate_expiration_timestamp_seconds
3
4# Certificates that failed to renew
5certmanager_certificate_ready_status{condition="False"}
6
7# ACME solver success rate
8rate(certmanager_http_acme_client_request_count{status="200"}[5m])

Frequently Asked Questions

What's the difference between ClusterIssuer and Issuer?

ClusterIssuer is cluster-scoped — any namespace can create Certificate resources that reference it. Issuer is namespace-scoped — it can only issue certificates in the same namespace it's deployed in. For shared clusters, use ClusterIssuer for production CAs (Let's Encrypt, Vault PKI). Use namespace-scoped Issuer when different teams need different CAs or different ACME accounts.

Why is my Certificate stuck in "Progressing"?

The most common causes:

  1. DNS-01 challenge: the Route 53 TXT record wasn't created (check IAM permissions with kubectl logs -n cert-manager deploy/cert-manager)
  2. HTTP-01 challenge: Let's Encrypt couldn't reach the challenge URL (check if port 80 is open, check Ingress class annotations)
  3. Rate limiting: Let's Encrypt imposes rate limits (5 certs per domain per week for duplicate certs on production) — use staging issuer for testing
  4. Check: kubectl describe certificaterequest -n <namespace> — the CertificateRequest status shows the ACME challenge status

Does cert-manager renew certificates automatically?

Yes. cert-manager watches all Certificate resources and renews them when renewBefore time remains until expiry (default 30 days). Let's Encrypt certs are valid 90 days; with the default renewBefore: 30d, cert-manager renews when 30 days remain (at the 60-day mark of the cert's life). The renewed cert is stored in the same Secret — applications reading the Secret via volume mount pick it up automatically. Applications that read the cert file once at startup need a restart after renewal.


For ingress-nginx that consumes TLS Secrets from cert-manager, see ingress-nginx in Production: Configuration, TLS, and Rate Limiting. For Gateway API TLS termination with cert-manager, see Kubernetes Gateway API: HTTPRoute, GRPCRoute, and the End of Ingress Annotations. For private CA hierarchy setup, trust-manager bundle distribution across namespaces, and cert-manager monitoring with Prometheus, see cert-manager in Production: TLS Automation for Kubernetes.

Running into certificate issues on EKS or need to set up cert-manager for a multi-team platform? Talk to us at Coding Protocols — we help platform teams automate TLS management so certificate expiry is never an on-call incident.

Related Topics

cert-manager
Kubernetes
TLS
Let's Encrypt
ACME
Security
Ingress
EKS

Read Next