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
- Level
- Practitioner · 7 steps
On this page
Before you start
- A Kubernetes cluster,
kubectland 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:
If something goes wrong
- The Certificate stays
False. Follow the chain:kubectl describethe 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 awaitInsteadOfSelfChecksolver option for that case (release notes). Otherwise check DNS, the ingress class and firewalls. secret not foundfor a ClusterIssuer. Its referenced Secrets must be in the cluster resource namespace,cert-managerby 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_INVALIDdespite a renewed Secret. The application mounted the certificate withsubPathor 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
- cert-manager: Helm installation: versions, CRDs and chart values.
- cert-manager: ACME issuers: HTTP-01 and DNS-01 solvers, profiles and external account binding.
- cert-manager: Certificate resource: renewal timing, key rotation and every field.
- Let’s Encrypt: staging environment: test endpoints and limits.
- RFC 8555: the ACME protocol.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.