Ingress with TLS

Terminate HTTPS and force redirect at the edge.

Intermediate12 min · lesson 4 of 24

An Ingress is the cluster’s HTTP front door: a controller (ingress-nginx, Traefik, and so on) watches Ingress objects and programs one reverse proxy that routes by host and path to backend Services. Terminating TLS there means a single place holds the certificate and speaks HTTPS to the world, then forwards to Services inside the cluster — instead of every backend pod owning a cert. For a security specialist the Ingress is where public exposure meets your internal Services, so it is where TLS and redirect policy get enforced.

How outside traffic reaches your pods
North-south traffic path from external user through Ingress, Service and EndpointSlice to Pods An external user sends an HTTPS request to the Ingress controller which terminates TLS, then routes in-cluster to a ClusterIP Service, an EndpointSlice of live pod IPs, and the backing Pods. HTTPS · encrypted route host / path select load-balance External User GET :443 client Ingress TLS terminated ingress controller single HTTPS front door Service ClusterIP · vIP stable virtual IP never changes Endpoint Slice live pod IPs real · changing Pods app backends
HTTPS (TLS) In-cluster routing Backend pods
Ingress terminates TLS and routes by host/path to a Service, which load-balances across the live pod IPs in its EndpointSlice.

The certificate and private key live in a TLS-type Secret (keys tls.crt and tls.key), which the Ingress references by name. Create the Secret first, then point the tls block at it and match the host.

terminal
# a TLS secret is just the cert + key under fixed keys
$ kubectl create secret tls payments-tls \
--cert=tls.crt --key=tls.key -n payments
$ kubectl -n payments get secret payments-tls -o jsonpath='{.type}'
kubernetes.io/tls
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payments
namespace: payments
spec:
ingressClassName: nginx
tls:
- hosts: [pay.acme.internal]
secretName: payments-tls # cert served for this host
rules:
- host: pay.acme.internal
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: payments-api, port: { number: 8080 } }

Force HTTPS and refuse weak TLS

Terminating TLS is pointless if the door still answers plaintext HTTP on :80. Turn on an SSL redirect so every HTTP request is 308-upgraded to HTTPS, and constrain the protocol versions and ciphers the controller will negotiate so you are not completing a TLS 1.0 handshake with whoever asks. In ingress-nginx these are annotations on the object plus controller-wide config; the exact keys differ per controller, which is why the docs tab matters.

ingress.yaml (annotations)
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # http -> https
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" # even without default backend
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.3 TLSv1.2"

Verify from outside the cluster: an HTTP request should return a redirect to HTTPS, and the served certificate should be the one from your Secret with the right subject. curl -v shows the handshake, the negotiated protocol version, and the certificate chain in one shot.

terminal
$ curl -sI http://pay.acme.internal/ # should redirect, not serve
HTTP/1.1 308 Permanent Redirect
Location: https://pay.acme.internal/
$ curl -v https://pay.acme.internal/ 2>&1 | grep -E "SSL connection|subject:"
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* subject: CN=pay.acme.internal
The cert is a Secret — treat it like one
The private key sits in an ordinary Kubernetes Secret, so the same rules apply: encrypt secrets at rest, keep secrets:get scoped tight in that namespace, and rotate the certificate before it expires. A leaked ingress key is a leaked identity for your whole hostname — anyone holding it can impersonate the site.