Kubernetes Ingress TLS with cert-manager and Let's Encrypt
Issue and auto-renew TLS certificates for Ingress with cert-manager, ACME, and a ClusterIssuer, and debug stuck orders before ACME rate-limits you.
An Ingress without TLS is a welcome mat. You terminate HTTPS at the edge so browsers stop warning, cookies stay secure, and you are not shipping credentials over cleartext inside the cluster boundary. cert-manager automates the boring part: talk to Let's Encrypt (or your private ACME), store the cert as a Secret, and renew it before it expires. The part that bites teams is not the happy path — it is a Certificate stuck in False while ACME rate-limits quietly stack up.
This walkthrough wires a ClusterIssuer, an Ingress that asks for a certificate, and the three kubectl checks that tell you whether ACME is progressing or stuck. Pair it with the networking lessons in Kubernetes fundamentals if Ingress itself is still fuzzy.
TLS terminates at the Ingress controller. Everything after that is in-cluster routing to Ready pods.
Each box is a Kubernetes object or external hop. If any box stalls, the Certificate stays unready and browsers keep failing.
Install cert-manager and create a ClusterIssuer
Install from the official manifests (pin a version — floating latest on your certificate factory is a supply-chain joke). Then declare a ClusterIssuer that uses HTTP-01 against the Ingress controller that faces the internet. The solver's ingress.class must match the controller actually receiving traffic — nginx, traefik, and AWS ALB each need different solver config.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata:name: letsencrypt-prodspec:acme:email: [email protected]server: https://acme-v02.api.letsencrypt.org/directoryprivateKeySecretRef:name: letsencrypt-prod-accountsolvers:- http01:ingress:class: nginx
Use letsencrypt-staging first (acme-staging-v02) while you debug. Staging certificates are untrusted in browsers, but they do not burn your production rate limit when you fat-finger a Challenge. Only switch to prod once kubectl describe challenge shows State: valid end to end.
Annotate the Ingress so a Certificate appears
The cleanest pattern: put hosts under spec.tls and let cert-manager's ingress-shim create the Certificate. The annotation pins which issuer to use. The secretName you declare is where cert-manager writes tls.crt and tls.key — the Ingress controller reads the same Secret on reload.
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: shopnamespace: prodannotations:cert-manager.io/cluster-issuer: letsencrypt-prodspec:ingressClassName: nginxtls:- hosts: [shop.example.com]secretName: shop-tlsrules:- host: shop.example.comhttp:paths:- path: /pathType: Prefixbackend:service:name: shopport:number: 80
kubectl get certificate -n prod shop-tlsNAME READY SECRET AGEshop-tls True shop-tls 2mkubectl describe certificate -n prod shop-tls | tail -20Events should show Order → Challenge → CertificateIssuedWhen READY stays False
Read the Certificate, then the Order, then the Challenge. HTTP-01 needs the ACME client to reach http://shop.example.com/.well-known/acme-challenge/... from the public internet. DNS mistakes, a WAF blocking that path, or the wrong ingress class are the usual villains — not cert-manager itself. curl -v the challenge URL from outside the cluster before you blame the controller.
For wildcard certs or internal-only hostnames, switch the ClusterIssuer solver to dns-01 against Route53, Cloudflare, or your DNS provider. HTTP-01 cannot validate *.example.com, and it fails when the service is reachable only inside the VPC.
Where this goes next
TLS at the Ingress is table stakes. Next: mTLS between services, short-lived certs from Vault PKI, and NetworkPolicies so a compromised pod cannot roam. The Kubernetes security (CKS) path covers admission, network policy, and runtime guards together.
Go deeper in a courseKubernetes security (CKS-aligned)NetworkPolicy, TLS, admission, and runtime hardening in one track.View course