Containers and platforms

How to get Let's Encrypt certificates in Kubernetes with cert-manager

By · Last verified

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:

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:

# 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
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).

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:

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
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:

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:

solvers:
  - dns01:
      cloudflare:
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token
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, or from any terminal:

curl ssl.sh/example.com

In the cluster:

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 (in development):

kubectl get certificates -A

Troubleshooting

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

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.
  • x509: certificate signed by unknown authority in clients: you are still serving the staging certificate; see x509: certificate signed by unknown authority.
  • too many certificates already issued: repeated deletes against production; see Let's Encrypt rate limits.
  • CAA record prevents issuance: see CAA record prevents issuance.
  • Secret exists but the controller serves its default certificate: the Ingress tls.secretName or namespace does not match the Secret.

FAQ

Why start with the staging issuer?

Staging has much higher rate limits, so mistakes in DNS, ingress class or solvers do not lock you out of production. Its certificates are not publicly trusted; clients report an unknown authority until you switch the annotation to the production issuer.

How do I get a wildcard certificate?

Use a DNS-01 solver. cert-manager has built-in solvers for several providers, including Cloudflare (cloudflare.apiTokenSecretRef) and Route 53; other DNS providers are supported through webhook solvers maintained by their communities.

When does cert-manager renew?

By default when two thirds of the certificate's lifetime has passed, so a 90-day certificate renews about 30 days before expiry. Set spec.renewBefore or spec.renewBeforePercentage on the Certificate to change that. The new Secret is picked up by the ingress controller or gateway automatically.

Sources

  1. cert-manager: Helm installation
  2. cert-manager: ACME
  3. cert-manager: HTTP01
  4. cert-manager: Securing Ingress resources
  5. cert-manager: Annotated Gateway resource
  6. Kubernetes blog: Ingress NGINX Retirement