---
# === IDENTITY ===
id: software/devops/k8s-cert-manager/2026
canonical_question: "How do I set up cert-manager with Let's Encrypt on Kubernetes for automatic TLS certificates?"
aliases:
  - "cert-manager Let's Encrypt Kubernetes setup"
  - "Kubernetes automatic TLS certificate management"
  - "cert-manager ClusterIssuer Let's Encrypt ACME"
  - "Kubernetes wildcard certificate DNS-01 challenge"
  - "cert-manager HTTP-01 vs DNS-01 solver"
  - "Kubernetes Ingress automatic HTTPS cert-manager"
  - "cert-manager troubleshoot certificate not issuing"
  - "Kubernetes TLS certificate auto-renewal Let's Encrypt"
entity_type: software_reference
domain: software > devops > Kubernetes Cert-Manager + Let's Encrypt
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.94
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "v1.18 (Jun 2025): default revisionHistoryLimit changed to 1, stale CertificateRequests garbage-collected on upgrade"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "cert-manager CRDs MUST be installed before any Issuer, ClusterIssuer, or Certificate resources -- applying them without CRDs silently fails"
  - "Wildcard certificates (*.example.com) require DNS-01 challenge -- HTTP-01 cannot validate wildcard domains"
  - "Let's Encrypt rate limits: 50 certificates per registered domain per week in production -- always test with staging first"
  - "The privateKeySecretRef secret is your ACME account identity -- deleting it forces re-registration and may hit rate limits"
  - "cert-manager requires Kubernetes 1.29+ for v1.18 and 1.31+ for v1.19 -- check compatibility matrix before installing"
  - "DNS-01 solver needs API credentials for your DNS provider -- store them in a Kubernetes Secret, never in the Issuer spec directly"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Using Gateway API with TLS instead of Ingress"
    use_instead: "software/devops/k8s-gateway-api/2026"
  - condition: "Need to debug a Pod stuck in CrashLoopBackOff or Pending"
    use_instead: "software/debugging/kubernetes-crashloopbackoff/2026"
  - condition: "Need general SSL/TLS certificate troubleshooting (not Kubernetes-specific)"
    use_instead: "software/debugging/ssl-tls-certificate-errors/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "challenge_type"
    question: "Which ACME challenge type do you need?"
    type: choice
    options: ["HTTP-01 (standard web domains)", "DNS-01 (wildcard or private clusters)", "Not sure"]
  - key: "dns_provider"
    question: "Which DNS provider hosts your domain?"
    type: choice
    options: ["Cloudflare", "AWS Route 53", "Google Cloud DNS", "Azure DNS", "DigitalOcean", "Other"]
  - key: "ingress_controller"
    question: "Which Ingress controller are you using?"
    type: choice
    options: ["NGINX Ingress Controller", "Traefik", "HAProxy", "AWS ALB", "GKE Ingress", "Other"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/k8s-cert-manager/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/devops/k8s-deployment-service-ingress/2026"
      label: "Kubernetes Deployment + Service + Ingress"
  related_to:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors"
    - id: "software/devops/docker-nginx-ssl/2026"
      label: "Docker NGINX + SSL"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors (generic, not K8s-specific)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "ACME Issuer Configuration"
    author: cert-manager project
    url: https://cert-manager.io/docs/configuration/acme/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src2
    title: "Troubleshooting Problems with ACME / Let's Encrypt Certificates"
    author: cert-manager project
    url: https://cert-manager.io/docs/troubleshooting/acme/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src3
    title: "Annotated Ingress Resource"
    author: cert-manager project
    url: https://cert-manager.io/docs/usage/ingress/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src4
    title: "DNS01 Challenge Solver Configuration"
    author: cert-manager project
    url: https://cert-manager.io/docs/configuration/acme/dns01/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src5
    title: "DNS Validation Tutorial"
    author: cert-manager project
    url: https://cert-manager.io/docs/tutorials/acme/dns-validation/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src6
    title: "Supported Releases"
    author: cert-manager project
    url: https://cert-manager.io/docs/releases/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
  - id: src7
    title: "HTTP01 Challenge Solver Configuration"
    author: cert-manager project
    url: https://cert-manager.io/docs/configuration/acme/http01/
    type: official_docs
    published: 2025-10-07
    reliability: authoritative
---

# Kubernetes: Cert-Manager + Let's Encrypt

## TL;DR

- **Bottom line**: cert-manager automates TLS certificate provisioning and renewal on Kubernetes using Let's Encrypt (ACME), supporting both HTTP-01 and DNS-01 challenges via Issuer/ClusterIssuer CRDs.
- **Key tool/command**: `kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml`
- **Watch out for**: Using production Let's Encrypt before validating with staging -- you will hit rate limits (50 certs/domain/week) and get locked out.
- **Works with**: Kubernetes 1.29+ (cert-manager v1.18), 1.31+ (cert-manager v1.19/1.20); NGINX Ingress, Traefik, HAProxy, AWS ALB, GKE Ingress.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- cert-manager CRDs MUST be installed before creating any Issuer, ClusterIssuer, or Certificate resources -- otherwise resources are silently ignored [src1]
- Wildcard certificates (`*.example.com`) REQUIRE the DNS-01 challenge solver -- HTTP-01 cannot validate wildcard domains [src4]
- Let's Encrypt production rate limits: 50 certificates per registered domain per week -- always validate your setup with the staging endpoint first [src1]
- The `privateKeySecretRef` Secret is your ACME account identity -- deleting it forces re-registration and may trigger rate limits [src1]
- DNS-01 solver credentials MUST be stored in Kubernetes Secrets, never hardcoded in the Issuer/ClusterIssuer spec [src4]
- cert-manager v1.18+ defaults `revisionHistoryLimit` to 1 -- upgrading garbage-collects all stale CertificateRequests automatically [src6]

## Quick Reference

| Resource | Kind | Scope | Purpose | Key Fields |
|---|---|---|---|---|
| Issuer | `cert-manager.io/v1` | Namespace | ACME account + solver config for one namespace | `spec.acme.server`, `spec.acme.solvers` |
| ClusterIssuer | `cert-manager.io/v1` | Cluster-wide | ACME account + solver config for all namespaces | Same as Issuer, but cluster-scoped |
| Certificate | `cert-manager.io/v1` | Namespace | Declares desired TLS certificate | `spec.dnsNames`, `spec.secretName`, `spec.issuerRef` |
| CertificateRequest | `cert-manager.io/v1` | Namespace | Auto-created; tracks individual issuance attempt | Read-only status resource |
| Order | `acme.cert-manager.io/v1` | Namespace | Auto-created; ACME order lifecycle | Tracks challenge state |
| Challenge | `acme.cert-manager.io/v1` | Namespace | Auto-created; one per domain to validate | Shows solver status and errors |
| HTTP-01 solver | solver type | N/A | Proves domain ownership via HTTP endpoint at `/.well-known/acme-challenge/` | `spec.acme.solvers[].http01.ingress` |
| DNS-01 solver | solver type | N/A | Proves domain ownership via `_acme-challenge` TXT record | `spec.acme.solvers[].dns01.<provider>` |
| Staging endpoint | Let's Encrypt | N/A | `acme-staging-v02.api.letsencrypt.org/directory` | Untrusted certs, high rate limits |
| Production endpoint | Let's Encrypt | N/A | `acme-v02.api.letsencrypt.org/directory` | Trusted certs, strict rate limits |
| Ingress annotation | `cert-manager.io/cluster-issuer` | Ingress | Auto-creates Certificate from Ingress TLS section | Triggers ingress-shim |
| Ingress annotation | `cert-manager.io/issuer` | Ingress | Same as above, references namespaced Issuer | Must be in same namespace |
| Renewal window | cert-manager default | N/A | Renews 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? (Cloudflare, Route53, CloudDNS, AzureDNS, DigitalOcean, RFC2136)
│   │   │   ├── YES → Configure dns01 solver with provider credentials
│   │   │   └── NO → Use webhook solver from community (40+ providers available)
│   │   └── Store provider API credentials in a Kubernetes Secret
│   └── NO ↓
├── Cluster publicly accessible on port 80?
│   ├── YES → Use HTTP-01 challenge (simplest setup)
│   │   └── Configure http01.ingress with your ingressClassName
│   └── 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. The Helm method provides more configuration options; kubectl apply is simpler for quick setups. [src1]

```bash
# 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 (cert-manager, cainjector, webhook) should be `Running`.

### 2. Create a ClusterIssuer for Let's Encrypt staging

Always start with staging to avoid rate limits. The staging endpoint issues untrusted certificates but has much higher rate limits for testing. [src1]

```yaml
# staging-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: your-email@example.com       # ACME account email (required)
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx      # Match your ingress controller
```

**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]

```yaml
# production-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: your-email@example.com
    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 (automatic method)

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

```yaml
# ingress-with-tls.yaml
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    # cert-manager stores the cert here
  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. Request a certificate manually (Certificate resource)

For more control, create a Certificate resource directly. This is required for wildcard certificates or when you need to specify additional options. [src5]

```yaml
# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: default
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - example.com
    - www.example.com
  # Optional: customize renewal
  # renewBefore: 720h    # 30 days (default)
  # duration: 2160h      # 90 days (Let's Encrypt default)
```

**Verify**: `kubectl describe certificate example-com-tls` -- Events should show `Certificate issued successfully`.

### 6. Configure DNS-01 for wildcard certificates (Cloudflare example)

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

```yaml
# cloudflare-api-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
type: Opaque
stringData:
  api-token: "your-cloudflare-api-token-here"
---
# wildcard-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-dns
spec:
  acme:
    email: your-email@example.com
    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
---
# wildcard-certificate.yaml
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`.

### 7. Configure DNS-01 for AWS Route 53

For AWS environments, use IAM credentials or IRSA (IAM Roles for Service Accounts). [src4]

```yaml
# route53-secret.yaml (if not using IRSA)
apiVersion: v1
kind: Secret
metadata:
  name: route53-credentials
  namespace: cert-manager
type: Opaque
stringData:
  secret-access-key: "YOUR_AWS_SECRET_ACCESS_KEY"
---
# route53-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-route53
spec:
  acme:
    email: your-email@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-route53-key
    solvers:
      - dns01:
          route53:
            region: us-east-1
            hostedZoneID: Z2FDTNDATAQYW2   # Your Route 53 hosted zone ID
            # Option A: Explicit credentials
            accessKeyIDSecretRef:
              name: route53-credentials
              key: access-key-id
            secretAccessKeySecretRef:
              name: route53-credentials
              key: secret-access-key
            # Option B: Use IRSA (preferred) -- omit accessKeyID/secretAccessKey
            # and annotate cert-manager ServiceAccount with IAM role
```

**Verify**: `kubectl describe clusterissuer letsencrypt-prod-route53` -- READY should be `True`.

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

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

> Full script: [yaml-mixed-http-01-dns-01-solvers-on-one-clusteris.yml](scripts/yaml-mixed-http-01-dns-01-solvers-on-one-clusteris.yml) (27 lines)

```yaml
# Input:  Cluster with both public-facing and wildcard domains
# Output: Single ClusterIssuer that routes challenges by domain type
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
# ... (see full script)
```

### YAML: Certificate with custom renewal settings

```yaml
# Input:  Need shorter renewal window or specific key algorithm
# Output: Certificate with fine-tuned settings

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: custom-cert
  namespace: default
spec:
  secretName: custom-cert-tls
  duration: 2160h       # 90 days
  renewBefore: 720h     # Renew 30 days before expiry
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - app.example.com
    - api.example.com
```

### Bash: Complete setup script for NGINX Ingress + cert-manager

> Full script: [bash-complete-setup-script-for-nginx-ingress-cert-.sh](scripts/bash-complete-setup-script-for-nginx-ingress-cert-.sh) (59 lines)

```bash
#!/bin/bash
# Input:  Fresh Kubernetes cluster with kubectl configured
# Output: cert-manager installed with staging + production issuers
set -euo pipefail
EMAIL="your-email@example.com"
# ... (see full script)
```

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

```yaml
# Input:  Want to delegate _acme-challenge to a less-privileged zone
# Output: ClusterIssuer using CNAME delegation strategy

# Step 1: Create DNS CNAME record:
#   _acme-challenge.example.com  CNAME  _acme-challenge.acme.example-delegated.com

# Step 2: Configure ClusterIssuer with cnameStrategy
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-delegated
spec:
  acme:
    email: your-email@example.com
    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
```

## Anti-Patterns

### Wrong: Using production Let's Encrypt endpoint from the start

```yaml
# BAD -- hitting production endpoint during testing/development
# will exhaust rate limits (50 certs/domain/week) and lock you out
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: dev@example.com
    privateKeySecretRef:
      name: letsencrypt-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
```

### Correct: Start with staging, switch to production after validation

```yaml
# GOOD -- use staging first, then switch server URL to production
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: dev@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
```

### Wrong: Trying HTTP-01 for wildcard certificates

```yaml
# BAD -- HTTP-01 CANNOT validate wildcard domains; this will fail silently
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard
spec:
  secretName: wildcard-tls
  issuerRef:
    name: letsencrypt-prod    # This issuer uses HTTP-01
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"         # Will never be issued via HTTP-01
```

### Correct: Use DNS-01 solver for wildcard domains

```yaml
# GOOD -- wildcard certificates require DNS-01 validation
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard
spec:
  secretName: wildcard-tls
  issuerRef:
    name: letsencrypt-prod-dns    # This issuer uses DNS-01
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"
    - "example.com"               # Include bare domain too
```

### Wrong: Hardcoding DNS provider credentials in the Issuer

```yaml
# BAD -- credentials visible to anyone with read access to the Issuer resource
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-bad
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: dev@example.com
    privateKeySecretRef:
      name: letsencrypt-key
    solvers:
      - dns01:
          cloudflare:
            email: admin@example.com
            apiKey: "abc123-my-actual-api-key"   # NEVER do this
```

### Correct: Store credentials in a Kubernetes Secret

> Full script: [correct-store-credentials-in-a-kubernetes-secret.yml](scripts/correct-store-credentials-in-a-kubernetes-secret.yml) (26 lines)

```yaml
# GOOD -- reference a Secret for credentials
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
# ... (see full script)
```

### Wrong: Missing tls.secretName in Ingress (ingress-shim does nothing)

```yaml
# BAD -- annotation is present but tls section has no secretName
# cert-manager's ingress-shim silently skips this Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: broken-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
  # Missing tls section entirely!
```

### Correct: Include tls section with secretName and hosts

```yaml
# GOOD -- tls section with secretName triggers cert-manager
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: working-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
                port:
                  number: 80
```

## Common Pitfalls

- **Certificate stuck in `False` READY state**: The most common cause is the ACME challenge failing. Walk the resource chain: Certificate -> CertificateRequest -> Order -> Challenge. Fix: `kubectl describe challenge <name>` to see the specific error. [src2]
- **HTTP-01 challenge returns 404**: The cert-manager solver Pod creates a temporary Ingress. If your ingress controller does not pick it up, the challenge fails. Fix: verify `ingressClassName` in the solver matches your controller, or use `acme.cert-manager.io/http01-edit-in-place: "true"` annotation. [src2]
- **DNS-01 TXT record not propagating**: cert-manager performs self-checks before notifying the ACME server. Internal DNS resolvers may not see the record. Fix: set `--dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53` and `--dns01-recursive-nameservers-only` on the cert-manager controller. [src4]
- **TLS handshake failure during HTTP-01**: The ingress controller requires a certificate to serve HTTPS, but the real cert is not issued yet. Fix: add `cert-manager.io/issue-temporary-certificate: "true"` annotation to get a self-signed placeholder while the real cert is provisioned. [src2]
- **`error registering ACME account: secrets already exists`**: A previous ACME account Secret conflicts with the new Issuer. Fix: delete the old Secret (`kubectl delete secret <privateKeySecretRef-name> -n cert-manager`) and re-apply the Issuer. [src2]
- **Rate limit exceeded on production Let's Encrypt**: You hit the 50 certs/domain/week limit. Fix: wait for the rate limit window to reset (7 days), and always use staging for testing. Check limits at `https://letsencrypt.org/docs/rate-limits/`. [src1]
- **Challenge pods not created**: cert-manager webhook or cainjector are not running. Fix: `kubectl get pods -n cert-manager` -- restart any crashed pods and check logs with `kubectl logs -n cert-manager deploy/cert-manager`. [src2]
- **Ingress annotation ignored (no Certificate created)**: The `tls` section in the Ingress spec is missing or `tls[].secretName` is empty. Fix: ensure every `tls` entry has a `secretName` and at least one `hosts` entry matching a rule. [src3]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (28 lines)

```bash
# Check cert-manager pods are running
kubectl get pods -n cert-manager
# Check ClusterIssuer/Issuer status
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod
# ... (see full script)
```

## Version History & Compatibility

| Version | Status | Release Date | K8s Support | Breaking Changes |
|---|---|---|---|---|
| v1.20 | Upcoming | Feb 24, 2026 | 1.32-1.35 | TBD |
| v1.19 | Current | Oct 07, 2025 | 1.31-1.35 | None major |
| v1.18 | Supported | Jun 10, 2025 | 1.29-1.33 | `revisionHistoryLimit` defaults to 1; stale CertificateRequests garbage-collected on upgrade |
| v1.17 | EOL | ~Feb 2025 | 1.28-1.31 | Structured logging replaces unstructured; ValidateCAA feature gate deprecated |
| v1.16 | EOL | ~Oct 2024 | 1.27-1.30 | Last version on OperatorHub |

cert-manager follows a 4-month release cadence. Only the last two minor versions are supported at any time. [src6]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| You need automatic TLS certificates on Kubernetes | You are not running Kubernetes | Certbot standalone or ACME client |
| You want Let's Encrypt auto-renewal (90-day certs) | You have enterprise CA with manual cert distribution | Your CA's certificate management tool |
| You manage multiple domains/services in a cluster | You have a single static site with manual cert management | Cloudflare SSL or hosting provider's built-in TLS |
| You need wildcard certificates with DNS-01 | Your cluster has no outbound internet access | Internal CA (Vault PKI, step-ca) |
| You want annotation-driven TLS on Ingress resources | You are using Gateway API with built-in cert management | Gateway API + TLS passthrough or GAMMA integration |

## Important Caveats

- Let's Encrypt certificates are valid for 90 days. cert-manager renews them 30 days before expiry by default. If cert-manager goes down for more than 60 days, your certificates will expire.
- HTTP-01 challenges require port 80 to be publicly accessible from the internet. If your cluster is behind a strict firewall or NAT, you must use DNS-01 instead.
- When using DNS-01 with cloud providers, the API token needs write access to DNS records. Follow the principle of least privilege: create a scoped token that can only modify TXT records in the target zone.
- cert-manager v1.18+ garbage-collects old CertificateRequest resources on upgrade. If you have monitoring or tooling that relies on historical CertificateRequest data, set `revisionHistoryLimit` explicitly before upgrading.
- The ACME `privateKeySecretRef` secret is the identity of your ACME account with Let's Encrypt. Back it up if you need to preserve your account across cluster migrations.
- dnsmasq with `--filterwin2k` flag (common in OpenWRT) drops SOA records, breaking cert-manager's DNS zone detection. Disable this flag or use `--dns01-recursive-nameservers` to bypass. [src4]

## Related Units

- [Kubernetes Deployment + Service + Ingress](/software/devops/k8s-deployment-service-ingress/2026)
- [SSL/TLS Certificate Errors](/software/debugging/ssl-tls-certificate-errors/2026)
- [Docker NGINX + SSL](/software/devops/docker-nginx-ssl/2026)
