<!-- https://pkizerotohero.com/guides/kubernetes-cert-manager/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Install cert-manager, choose Issuer or ClusterIssuer, get Let's Encrypt certificates for Ingress via staging then production, and debug stuck challenges.

Source: https://pkizerotohero.com/guides/kubernetes-cert-manager/ · Last reviewed: 2026-10-20

How-to guide · Automate renewal

# How to automate certificates in Kubernetes with cert-manager

**In one sentence:**

cert-manager issues Let's Encrypt certificates for your Kubernetes Ingresses or Certificate resources, stores them in Secrets and renews them before they expire.

Task

[Automate renewal](https://pkizerotohero.com/guides/#task-automate)

Platforms

- Kubernetes
- Linux

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Developer](https://pkizerotohero.com/start/#developer)

Level

Practitioner · 7 steps

## Before you start

- A Kubernetes cluster, `kubectl` and Helm 3, and rights to install cluster-wide resources (CRDs).
- An ingress controller (or Gateway API implementation) reachable from the internet on port 80, and DNS records for your names pointing to it.
- An email address for the Let’s Encrypt account.
- For wildcard names: the DNS-01 challenge, see [wildcard certificates with DNS-01](https://pkizerotohero.com/guides/wildcard-dns-01/).

Why it matters: from 15 March 2029, publicly trusted TLS certificates may be valid for at most 47 days. In a cluster with dozens of hostnames, only a controller that renews on its own keeps up.

## Steps

### 1. Install cert-manager with Helm

The [installation guide](https://cert-manager.io/docs/installation/helm/) recommends the OCI chart. Use the version it lists; on 20 October 2026 that was v1.21.2:

```bash
helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --version v1.21.2 \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
kubectl get pods -n cert-manager
```

You should see the controller, the `cainjector` and the `webhook` pods running. To let cert-manager follow the CA’s renewal advice ([ARI](https://pkizerotohero.com/handbook/glossary/#ari), RFC 9773, on the IETF standards track), add `--set featureGates="ACMEUseARI=true"`. In v1.21 that feature gate is alpha and off by default ([v1.21 release notes](https://cert-manager.io/docs/releases/release-notes/release-notes-1.21/)).

### 2. Choose Issuer or ClusterIssuer

|  | Issuer | ClusterIssuer |
| --- | --- | --- |
| Scope | One namespace: “it is not possible to issue certificates from an Issuer in a different namespace” | All namespaces |
| Its Secrets (ACME account key, DNS credentials) | In its own namespace | In the cluster resource namespace, `cert-manager` by default |
| Good for | Teams that manage their own CA settings | One shared Let’s Encrypt setup for the whole cluster |

Both quotes and rules are from the cert-manager [Issuer concepts](https://cert-manager.io/docs/concepts/issuer/) and [configuration](https://cert-manager.io/docs/configuration/) pages. The examples below use a ClusterIssuer.

### 3. Create a staging ClusterIssuer

Start on Let’s Encrypt’s staging CA, which has much higher rate limits and issues untrusted test certificates ([cert-manager ACME configuration](https://cert-manager.io/docs/configuration/acme/)):

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

```bash
kubectl apply -f clusterissuer-staging.yaml
kubectl get clusterissuer letsencrypt-staging
```

Wait until `READY` is `True`. cert-manager has then registered an ACME account and stored its key in the Secret you named.

### 4. Request a certificate

The simplest way is an annotation on the Ingress. cert-manager reads the `tls` section and creates the certificate for those hosts ([securing Ingress resources](https://cert-manager.io/docs/usage/ingress/)):

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

Or declare a `Certificate` yourself, which also works for workloads without an Ingress ([Certificate resource](https://cert-manager.io/docs/usage/certificate/)):

```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: www-example-com
  namespace: my-app
spec:
  secretName: www-example-com-tls
  dnsNames:
  - www.example.com
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
```

Gateway API resources work too, once Gateway API support is turned on in cert-manager ([Gateway usage](https://cert-manager.io/docs/usage/gateway/)).

### 5. Watch it being issued

Each request flows through four resources: Certificate, CertificateRequest, Order and Challenge.

```bash
kubectl get certificate -n my-app
kubectl describe certificate www-example-com -n my-app
kubectl get certificaterequests,orders,challenges -n my-app
```

The Certificate turns `READY True` when the Secret holds the key in `tls.key` and the certificate with its chain in `tls.crt`.

### 6. Switch to production

Create a second ClusterIssuer, `letsencrypt-prod`, identical except for its name, its account Secret and the server `https://acme-v02.api.letsencrypt.org/directory`. Then change the annotation or `issuerRef` to `letsencrypt-prod`. cert-manager notices the change and issues a trusted certificate into the same Secret.

### 7. Understand renewal

By default cert-manager renews “2/3 through the X.509 certificate’s duration”; `renewBefore` or `renewBeforePercentage` change that. Since v1.18 the default `rotationPolicy` is `Always`, so each renewal gets a new private key. Ingress controllers watch the Secret and pick up the new certificate. Pods that mount the Secret as a volume see the new files, except with a `subPath` mount ([Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)), but the application itself must reread them.

## Check it worked

Decode what is stored, then check what clients receive:

```bash
kubectl get secret www-example-com-tls -n my-app -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -issuer -enddate
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | grep -E '^ [0-9] s:|^   i:|Verify return code'
```

With staging, the issuer is a staging intermediate and verification fails, which is expected. With production you want two certificates and `Verify return code: 0 (ok)`. The chain checker shows the same:

*Interactive tool: Chain checker (runs in the browser at [/tools/chain/](https://pkizerotohero.com/tools/chain/))*

## If something goes wrong

- **The Certificate stays `False`.** Follow the chain: `kubectl describe` the CertificateRequest, then the Order, then the Challenge. The Challenge’s status says why it is waiting.
- **The HTTP-01 self-check fails.** Before asking the CA, cert-manager fetches `/.well-known/acme-challenge/…` itself. Inside the cluster that can fail on split-horizon DNS or NAT hairpinning even when the internet can reach the path; v1.21 added a `waitInsteadOfSelfCheck` solver option for that case ([release notes](https://cert-manager.io/docs/releases/release-notes/release-notes-1.21/)). Otherwise check DNS, the ingress class and firewalls.
- **`secret not found` for a ClusterIssuer.** Its referenced Secrets must be in the cluster resource namespace, `cert-manager` by default, not in the application’s namespace.
- **Rate limits.** Let’s Encrypt allows up to 5 certificates for the exact same set of names every 7 days and up to 5 failed validations per name, per account, per hour. Debug on staging; do not delete and recreate Certificates in a loop.
- **Clients inside the cluster fail with [`x509: certificate signed by unknown authority`](https://pkizerotohero.com/errors/go-x509-unknown-authority/).** You are still on staging, or the client image lacks current roots.
- **[`NET::ERR_CERT_DATE_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/) despite a renewed Secret.** The application mounted the certificate with `subPath` or never rereads it (step 7).

Note

Not only Let's Encrypt

The same Certificate resources work with other issuers: a private CA through the `CA` issuer, HashiCorp Vault, or a private ACME server such as step-ca, by changing only the issuer. See [A CA of your own](https://pkizerotohero.com/handbook/private-ca/).

## Why this works

cert-manager is an [ACME](https://pkizerotohero.com/handbook/glossary/#acme) client running as a Kubernetes controller. For each Certificate it generates a key, creates an ACME order, answers the [ACME challenge](https://pkizerotohero.com/handbook/glossary/#acme-challenge) by creating a temporary solver Pod and Ingress route, then finalizes the order with a CSR and stores the result in a Secret. A timer per Certificate starts the same flow again before expiry. See [ACME and the 47-day schedule](https://pkizerotohero.com/handbook/acme/), [Proving you own the name](https://pkizerotohero.com/handbook/domain-validation/) and [Inventory, expiry and renewal](https://pkizerotohero.com/handbook/certificate-lifecycle/).

## Go deeper

- [cert-manager: Helm installation](https://cert-manager.io/docs/installation/helm/): versions, CRDs and chart values.
- [cert-manager: ACME issuers](https://cert-manager.io/docs/configuration/acme/): HTTP-01 and DNS-01 solvers, profiles and external account binding.
- [cert-manager: Certificate resource](https://cert-manager.io/docs/usage/certificate/): renewal timing, key rotation and every field.
- [Let’s Encrypt: staging environment](https://letsencrypt.org/docs/staging-environment/): test endpoints and limits.
- [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555): the ACME protocol.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/kubernetes-cert-manager/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 20: ACME and the 47-day schedule](https://pkizerotohero.com/handbook/acme/)
- [Chapter 19: Inventory, expiry and renewal](https://pkizerotohero.com/handbook/certificate-lifecycle/)
- [Chapter 11: Proving you own the name: DV, OV, EV and CAA](https://pkizerotohero.com/handbook/domain-validation/)

### Error pages

- [x509: certificate signed by unknown authority](https://pkizerotohero.com/errors/go-x509-unknown-authority/)
- [verify error:num=21:unable to verify the first certificate](https://pkizerotohero.com/errors/openssl-verify-error-21/)
- [NET::ERR\_CERT\_DATE\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Lifetime calendar](https://pkizerotohero.com/tools/lifetime/): See how long a certificate may live on any date, and when to renew.

### Topics

- [Lifecycle and automation](https://pkizerotohero.com/topics/lifecycle-and-automation/)

## More guides: Automate renewal

- [How to get and renew Let's Encrypt certificates with certbot](https://pkizerotohero.com/guides/certbot-lets-encrypt/)
- [How to automate certificates on Windows with win-acme](https://pkizerotohero.com/guides/win-acme-windows/)
- [How to get a wildcard certificate with DNS-01](https://pkizerotohero.com/guides/wildcard-dns-01/)
