Containers and platforms
How to get Let's Encrypt certificates in Kubernetes with cert-manager
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
kubectlaccess 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/AAAArecords forexample.compointing 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
ingressClassNamedoes 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.secretNameor 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.