Kubernetes: Cert-Manager + Let's Encrypt

Type: Software Reference Confidence: 0.94 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

ResourceKindScopePurposeKey Fields
Issuercert-manager.io/v1NamespaceACME account + solver config for one namespacespec.acme.server, spec.acme.solvers
ClusterIssuercert-manager.io/v1Cluster-wideACME account + solver config for all namespacesSame as Issuer, but cluster-scoped
Certificatecert-manager.io/v1NamespaceDeclares desired TLS certificatespec.dnsNames, spec.secretName, spec.issuerRef
CertificateRequestcert-manager.io/v1NamespaceAuto-created; tracks individual issuance attemptRead-only status resource
Orderacme.cert-manager.io/v1NamespaceAuto-created; ACME order lifecycleTracks challenge state
Challengeacme.cert-manager.io/v1NamespaceAuto-created; one per domain to validateShows solver status and errors
HTTP-01 solversolver typeN/AProves domain ownership via HTTP endpointspec.acme.solvers[].http01.ingress
DNS-01 solversolver typeN/AProves domain ownership via TXT recordspec.acme.solvers[].dns01.<provider>
Staging endpointLet's EncryptN/AUntrusted certs, high rate limitsacme-staging-v02.api.letsencrypt.org/directory
Production endpointLet's EncryptN/ATrusted certs, strict rate limitsacme-v02.api.letsencrypt.org/directory
Ingress annotationcert-manager.io/cluster-issuerIngressAuto-creates Certificate from Ingress TLS sectionTriggers ingress-shim
Ingress annotationcert-manager.io/issuerIngressSame, references namespaced IssuerMust be in same namespace
Renewal windowcert-manager defaultN/ARenews 30 days before expiry (LE certs last 90 days)Configurable via spec.renewBefore

Decision Tree

START: Need TLS certificates on Kubernetes?
├── Single namespace only?
│   ├── YES → Use Issuer (namespace-scoped)
│   └── NO → Use ClusterIssuer (cluster-wide)
├── Need wildcard certificate (*.example.com)?
│   ├── YES → MUST use DNS-01 challenge
│   │   ├── DNS provider supported in-tree?
│   │   │   ├── YES → Configure dns01 solver with provider credentials
│   │   │   └── NO → Use webhook solver from community
│   │   └── Store provider API credentials in a Kubernetes Secret
│   └── NO ↓
├── Cluster publicly accessible on port 80?
│   ├── YES → Use HTTP-01 challenge (simplest setup)
│   └── NO → Use DNS-01 challenge (works behind firewalls/NAT)
├── Want automatic Certificate creation from Ingress?
│   ├── YES → Add cert-manager.io/cluster-issuer annotation to Ingress
│   └── NO → Create Certificate resource manually
└── DEFAULT → Start with staging endpoint, switch to production after validation

Step-by-Step Guide

1. Install cert-manager

Install cert-manager and its CRDs into the cluster. Helm is recommended for production; kubectl apply is simpler for quick setups. [src1]

# Option A: Helm (recommended for production)
helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.17.2 \
  --set crds.enabled=true

# Option B: kubectl apply (quick setup)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml

Verify: kubectl get pods -n cert-manager -- all three Pods should be Running

2. Create a ClusterIssuer for Let's Encrypt staging

Always start with staging to avoid rate limits. Staging issues untrusted certificates but has much higher rate limits. [src1]

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: [email protected]
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Verify: kubectl get clusterissuer letsencrypt-staging -o wide -- READY should be True

3. Create a ClusterIssuer for Let's Encrypt production

Switch to production once staging works. This endpoint issues browser-trusted certificates. [src1]

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Verify: kubectl describe clusterissuer letsencrypt-prod -- Status should show The ACME account was registered with the ACME server

4. Request a certificate via Ingress annotation

The ingress-shim sub-component watches for annotated Ingress resources and automatically creates Certificate resources. [src3]

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-example-com-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-service
                port:
                  number: 80

Verify: kubectl get certificate -n default -- READY should become True within 1-2 minutes

5. Configure DNS-01 for wildcard certificates

Wildcard certificates require DNS-01 validation. Create a Secret with DNS provider API credentials, then configure the solver. [src4]

apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
type: Opaque
stringData:
  api-token: "your-cloudflare-api-token-here"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-dns
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-dns-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: default
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: letsencrypt-prod-dns
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"
    - "example.com"

Verify: kubectl get challenges -- should show challenges progressing from pending to valid

Code Examples

YAML: Mixed HTTP-01 + DNS-01 solvers on one ClusterIssuer

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-mixed
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-mixed-key
    solvers:
      - selector:
          dnsNames:
            - "*.example.com"
        dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
      - selector: {}
        http01:
          ingress:
            ingressClassName: nginx

YAML: CNAME delegation for DNS-01 (security best practice)

# Create CNAME: _acme-challenge.example.com -> _acme-challenge.acme.delegated.com
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-delegated
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-delegated-key
    solvers:
      - dns01:
          cnameStrategy: Follow
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token

Bash: Complete setup script

Full script: setup-cert-manager.sh (37 lines)

Anti-Patterns

Wrong: Using production Let's Encrypt from the start

# BAD -- hitting production endpoint during testing
# will exhaust rate limits (50 certs/domain/week)
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory

Correct: Start with staging, switch to production after validation

# GOOD -- use staging first
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory

Wrong: HTTP-01 for wildcard certificates

# BAD -- HTTP-01 CANNOT validate wildcard domains
dnsNames:
  - "*.example.com"     # Will never be issued via HTTP-01

Correct: Use DNS-01 solver for wildcard domains

# GOOD -- wildcard requires DNS-01
solvers:
  - dns01:
      cloudflare:
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token

Wrong: Missing tls.secretName in Ingress

# BAD -- annotation present but no tls section
# cert-manager's ingress-shim silently skips this
metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  rules:
    - host: app.example.com
  # Missing tls section entirely!

Correct: Include tls section with secretName and hosts

# GOOD -- tls section with secretName triggers cert-manager
spec:
  tls:
    - hosts:
        - app.example.com
      secretName: app-example-com-tls
  rules:
    - host: app.example.com

Common Pitfalls

Diagnostic Commands

# Check cert-manager pods are running
kubectl get pods -n cert-manager

# Check ClusterIssuer/Issuer status
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod

# List all certificates and their READY status
kubectl get certificates --all-namespaces

# Follow issuance chain: Certificate -> CertificateRequest -> Order -> Challenge
kubectl describe certificate <name> -n <namespace>
kubectl get certificaterequest -n <namespace>
kubectl describe order <name> -n <namespace>
kubectl get challenges --all-namespaces
kubectl describe challenge <name> -n <namespace>

# Check TLS secret was created and inspect certificate
kubectl get secret <secretName> -n <namespace> -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text

# View cert-manager controller logs
kubectl logs -n cert-manager deploy/cert-manager --tail=100

# Test DNS-01 TXT record propagation
dig _acme-challenge.example.com TXT +short

Version History & Compatibility

VersionStatusRelease DateK8s SupportBreaking Changes
v1.20UpcomingFeb 24, 20261.32-1.35TBD
v1.19CurrentOct 07, 20251.31-1.35None major
v1.18SupportedJun 10, 20251.29-1.33revisionHistoryLimit defaults to 1; stale CertificateRequests garbage-collected
v1.17EOL~Feb 20251.28-1.31Structured logging; ValidateCAA deprecated
v1.16EOL~Oct 20241.27-1.30Last version on OperatorHub

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
You need automatic TLS certificates on KubernetesYou are not running KubernetesCertbot standalone or ACME client
You want Let's Encrypt auto-renewal (90-day certs)You have enterprise CA with manual distributionYour CA's certificate management tool
You manage multiple domains/services in a clusterSingle static site with manual cert managementCloudflare SSL or hosting provider built-in TLS
You need wildcard certificates with DNS-01Cluster has no outbound internet accessInternal CA (Vault PKI, step-ca)
You want annotation-driven TLS on IngressUsing Gateway API with built-in cert managementGateway API + TLS passthrough

Important Caveats

Related Units