# cert-manager: Let's Encrypt TLS certificates for Kubernetes

> Install cert-manager with Helm, create Let's Encrypt staging and production ClusterIssuers, issue certificates for an Ingress or Gateway and debug orders.

Source: https://ssl.sh/guides/cert-manager-kubernetes/
Last updated: 2026-11-10

cert-manager issues Let's Encrypt certificates for hosts exposed through a Kubernetes Ingress or Gateway, stores them in Secrets that the controller serves, and renews them on its own. Install it with Helm, create a ClusterIssuer and annotate the Ingress.

The steps apply to cert-manager 1.18 and later (1.21.x current as of November 2026) on any supported Kubernetes version, with Helm 3.

## Prerequisites

The ACME HTTP-01 challenge must reach your ingress controller or gateway from the internet.

- A Kubernetes cluster and `kubectl` access with cluster-admin rights; Helm 3 installed locally.
- An ingress controller or Gateway API implementation with a public IP, for example `203.0.113.10`. If you still run the community ingress-nginx controller, plan a migration: it was retired in March 2026.
- DNS `A`/`AAAA` records for `example.com` pointing at that public IP.
- Port 80 open from the internet to the controller (for HTTP-01).

## Step 1: Install cert-manager with Helm

The Helm chart installs cert-manager and, with `crds.enabled=true`, its CRDs:

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

Replace `v1.21.3` with the version shown in the cert-manager installation docs. Three pods (controller, webhook, cainjector) should become Ready.

## Step 2: Create staging and production ClusterIssuers

A ClusterIssuer tells cert-manager which ACME server and challenge solver to use. Create one for Let's Encrypt staging and one for production:

```yaml
# issuers.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
```

```bash
kubectl apply -f issuers.yaml
kubectl get clusterissuer
```

Set `ingressClassName` to your controller's class. To request the 45-day profile, add `profile: tlsserver` under `acme:` (see [Let's Encrypt certificate profiles](https://ssl.sh/guides/lets-encrypt-certificate-profiles/)).

## Step 3: Request a certificate through the Ingress

Annotate the Ingress and declare the TLS host and Secret name; cert-manager creates the Certificate for you:

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

```bash
kubectl apply -f ingress.yaml
kubectl get certificate example-com-tls -w
```

When `READY` is `True` with the staging issuer, change the annotation to `letsencrypt-prod` and delete the Secret so a trusted certificate is issued:

```bash
kubectl annotate ingress web cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite
kubectl delete secret example-com-tls
```

With Gateway API, enable cert-manager's Gateway support and put the same `cert-manager.io/cluster-issuer` annotation on the Gateway; listeners with `tls.certificateRefs` get certificates (see the cert-manager Gateway docs).

## Step 4 (optional): DNS-01 for wildcards

Wildcard certificates need a DNS-01 solver in the issuer. For Cloudflare, add the solver and store the API token in a Secret:

```yaml
solvers:
  - dns01:
      cloudflare:
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token
```

```bash
kubectl create secret generic cloudflare-api-token -n cert-manager \
  --from-literal=api-token=your-cloudflare-api-token
```

The Secret must live in the cert-manager namespace for a ClusterIssuer.

## Verify

The host should present a Let's Encrypt certificate once the Certificate shows `Ready=True`. Check the public endpoint with the [SSL checker](https://ssl.sh/tools/ssl-checker/), or from any terminal:

```bash
curl ssl.sh/example.com
```

In the cluster:

```bash
kubectl describe certificate example-com-tls
kubectl get secret example-com-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -issuer -dates
```

## Automatic renewal

cert-manager renews each Certificate when two thirds of its lifetime has elapsed (configurable with `renewBefore` or `renewBeforePercentage`) and updates the Secret; ingress controllers and gateways reload it automatically. Because the timing is relative to the actual lifetime, nothing needs to change when Let's Encrypt moves to 64-day certificates on February 10, 2027 or 45-day certificates on February 16, 2028. Keep cert-manager upgraded, watch for `Ready=False` Certificates, and add public hosts to [ssl.sh monitoring](https://ssl.sh/monitor/) (in development):

```bash
kubectl get certificates -A
```

## Troubleshooting

Follow the chain Certificate → CertificateRequest → Order → Challenge to find where it stops:

```bash
kubectl get certificaterequest,order,challenge -A
kubectl describe challenge -A
```

- **Waiting for HTTP-01 challenge propagation: wrong status code 404**: the solver's `ingressClassName` does not match your controller, or port 80 does not reach it; the causes match [certbot timeout during connect](https://ssl.sh/errors/certbot-timeout-during-connect/).
- **x509: certificate signed by unknown authority** in clients: you are still serving the staging certificate; see [x509: certificate signed by unknown authority](https://ssl.sh/errors/x509-certificate-signed-by-unknown-authority/).
- **too many certificates already issued**: repeated deletes against production; see [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **CAA record prevents issuance**: see [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).
- **Secret exists but the controller serves its default certificate**: the Ingress `tls.secretName` or namespace does not match the Secret.
