Ingress TLS in Kubernetes terminates HTTPS at the ingress controller by referencing a TLS secret that holds a certificate and private key, matched to the hostnames in the tls block of your Ingress resource. Get three things aligned, the secret in the right namespace, the hosts list matching both the certificate's SAN and the routing rules, and the controller actually configured for TLS, and it works. Miss any one and you get a browser certificate warning, a default self-signed cert, or a plain 404. This guide covers the mechanics and the failure modes.
How TLS Termination Works
In the common setup, TLS terminates at the ingress controller (NGINX Ingress, Traefik, HAProxy, or a cloud load balancer). The client establishes HTTPS to the controller, the controller decrypts, and traffic flows to your pods over the cluster network, often as plain HTTP. That is fine for most architectures because the pod network is inside your security boundary; if it is not, you layer a service mesh with mTLS on top, which is a separate concern from ingress TLS.
The certificate and key live in a Kubernetes Secret of type kubernetes.io/tls, with two keys: tls.crt and tls.key.
A Minimal Working Example
Create the TLS secret (here from existing PEM files):
kubectl create secret tls app-tls \
--cert=fullchain.pem \
--key=privkey.pem \
--namespace=production
Reference it in the Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: production
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
Three things must agree: the hostname in tls.hosts, the hostname in rules.host, and a Subject Alternative Name on the certificate itself. When they diverge, the symptoms are confusing, which is why most of the debugging section below is about mismatches.
Automating Certificates with cert-manager
Nobody should rotate certificates by hand. cert-manager is the standard: it watches for Certificate resources (or annotations on Ingress objects), requests certs from an issuer like Let's Encrypt, solves the ACME challenge, and writes the resulting TLS secret, then renews automatically before expiry.
A ClusterIssuer for Let's Encrypt using HTTP-01:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Then annotate the Ingress and cert-manager fills in the secret:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
For wildcard certificates you need the DNS-01 solver instead, because Let's Encrypt will not issue wildcards over HTTP-01. That requires giving cert-manager API credentials for your DNS provider.
The Common Mistakes
These account for the majority of k8s ingress TLS support questions:
Secret in the wrong namespace. A TLS secret is only visible to Ingress resources in the same namespace. Referencing app-tls from a namespace that does not contain it silently falls back to the controller's default certificate, producing a "not secure" warning with a cert for the wrong host.
Hostname mismatch. The cert's SAN says www.example.com, the tls.hosts says example.com. Browsers reject it. Certificates match SANs exactly (with wildcard rules); there is no fuzzy matching.
Forgetting the tls block entirely. People add the certificate secret and the routing rule but omit the spec.tls section. Without it the controller serves HTTP only or its default self-signed cert. The tls block is what wires the secret to the host.
HTTP-01 challenge blocked. cert-manager stays stuck "pending" because the ACME challenge on /.well-known/acme-challenge/ cannot reach the cluster, usually a firewall, a missing DNS record, or an ingress rule that intercepts the path. Check kubectl describe challenge.
Wrong ingressClassName. With multiple controllers installed, omitting or misnaming ingressClassName means no controller claims the Ingress, so nothing serves it. The result looks like a certificate problem but is a routing problem.
Expired certs from a dead cert-manager. Automation lulls teams into not monitoring expiry. If cert-manager's renewal fails silently (rate limits, DNS changes, revoked API tokens), certs expire on schedule. Alert on certificate expiry independently of cert-manager's own health.
Verifying It Actually Works
Do not trust the browser padlock alone. Check the served certificate from the command line:
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Confirm the subject matches your host, the issuer is who you expect (Let's Encrypt, not the controller's default), and the notAfter date is comfortably in the future. Building this check into a synthetic monitor catches expiry and misconfiguration before users do.
FAQ
How does TLS work with Kubernetes Ingress?
The ingress controller terminates HTTPS using a certificate and key stored in a kubernetes.io/tls secret. The Ingress resource's tls block maps hostnames to that secret. Traffic from client to controller is encrypted; traffic from controller to pods is typically plain HTTP within the cluster.
Why is my Ingress serving the wrong certificate?
Almost always a namespace or hostname mismatch. The TLS secret must live in the same namespace as the Ingress, and the certificate's SAN must exactly match the hostname in both tls.hosts and the routing rule. Otherwise the controller falls back to its default certificate.
Should I use cert-manager for Ingress TLS?
Yes, for anything beyond a one-off. cert-manager requests, installs, and renews certificates automatically from issuers like Let's Encrypt, eliminating manual rotation. Use the HTTP-01 solver for single hostnames and DNS-01 for wildcards.
Do I need TLS between the ingress and my pods?
Not usually. Most architectures treat the in-cluster network as trusted and terminate TLS at the ingress. If your threat model requires encryption inside the cluster, add a service mesh with mutual TLS, which is separate from ingress TLS configuration.