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
Platforms
  • Kubernetes
  • Linux
Who it is for
Level
Practitioner · 7 steps
On this page
  1. Before you start
  2. Steps
  3. 1. Install cert-manager with Helm
  4. 2. Choose Issuer or ClusterIssuer
  5. 3. Create a staging ClusterIssuer
  6. 4. Request a certificate
  7. 5. Watch it being issued
  8. 6. Switch to production
  9. 7. Understand renewal
  10. Check it worked
  11. If something goes wrong
  12. Why this works
  13. Go deeper

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.

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 recommends the OCI chart. Use the version it lists; on 20 October 2026 that was v1.21.2:

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

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

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

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

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

5. Watch it being issued

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

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), but the application itself must reread them.

Check it worked

Decode what is stored, then check what clients receive:

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:

Try it: Chain checkerOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

Every certificate the server sends, leaf first. The output of openssl s_client -showcerts -connect example.com:443 works as it is: we ignore the text around the certificates.

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). 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. You are still on staging, or the client image lacks current roots.
  • NET::ERR_CERT_DATE_INVALID despite a renewed Secret. The application mounted the certificate with subPath or never rereads it (step 7).

Why this works

cert-manager is an ACME client running as a Kubernetes controller. For each Certificate it generates a key, creates an ACME order, answers the 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, Proving you own the name and Inventory, expiry and renewal.

Go deeper

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9