Why TLS on a Route is a choice
Security asks "is payments traffic encrypted all the way to the pod?", the browser says "certificate not trusted", and curl says "wrong version number". All three are questions about where TLS ends. On a Route you choose that explicitly.
What you need to know already: the TLS handshake, certificate chains, CAs, SNI and curl -v / --cacert (9.15, 9.17); TLS termination at an Ingress (16.22); Secrets of type kubernetes.io/tls (15.33); Routes, the router and the 503 page (30.13).
The words you need first
- Terminate TLS - decrypt the connection. Whoever terminates holds the private key.
- edge / passthrough / reencrypt - the three Route termination types (below).
- Service CA - a certificate authority that runs inside every OpenShift cluster and hands out certificates for Service names (
<svc>.<ns>.svc) on request. - Insecure policy - what a TLS Route does with a plain
http://request.
Where does TLS end?
A Route decides who terminates TLS - the router, or your pod, or both:
edge client --TLS--> router --HTTP--> pod
passthrough client --------TLS-------------> pod (the router only reads SNI)
reencrypt client --TLS--> router --TLS--> pod
oc create route TYPE NAME --service=SVC makes a TLS Route of that type; --insecure-policy sets what happens to plain HTTP:
# the TLS mission's routes (payments = its HTTPS backend; the -n openshift-config-managed read as kubeadmin)
oc create route edge web-tls --service=web --insecure-policy=Redirect
route.route.openshift.io/web-tls created
oc create route passthrough payments --service=payments
route.route.openshift.io/payments created
oc create route reencrypt payments-re --service=payments
route.route.openshift.io/payments-re created
oc get routes
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
payments payments-rt.apps.ocp.lab payments https passthrough None
payments-re payments-re-rt.apps.ocp.lab payments https reencrypt None
web-tls web-tls-rt.apps.ocp.lab web 80-8080 edge/Redirect None
edge
The router holds the certificate and talks plain HTTP to the pod. With no certificate on the Route, the router presents its default certificate: a wildcard for *.apps.<domain> signed by the Ingress Operator's own CA.
$ curl https://web-tls-rt.apps.ocp.lab/
curl: (60) SSL certificate problem: self-signed certificate in certificate chain
More details here: https://curl.se/docs/sslcerts.html
...
$ curl -sv https://web-tls-rt.apps.ocp.lab/ -k 2>&1 | grep -E 'subject:|issuer:'
* subject: CN=*.apps.ocp.lab
* issuer: CN=ingress-operator@1788984003
curl error 60 means "I cannot verify this certificate" (9.17); -k tells curl to skip the check. The fix is not -k. The CA is published in the cluster; extract it and trust it. oc extract writes each key of a ConfigMap or Secret into a file; --to=DIR picks the directory:
oc extract configmap/default-ingress-cert -n openshift-config-managed --to=/tmp
/tmp/ca-bundle.crt
curl --cacert /tmp/ca-bundle.crt https://web-tls-rt.apps.ocp.lab/ -o /dev/null -w '%{http_code}\n'
200
Production clusters replace that default with a company or public wildcard certificate (an IngressController defaultCertificate), so browsers trust every edge route out of the box.
The insecure policy
What happens on port 80 for a TLS route - spec.tls.insecureEdgeTerminationPolicy:
| policy | curl http://host/ |
|---|---|
unset / None (default) | 503 "Application is not available" - there is no HTTP route for that host |
Redirect | 302 Found, Location: https://host/... |
Allow | served over plain HTTP too (edge and reencrypt only) |
curl -sI http://web-tls-rt.apps.ocp.lab/
HTTP/1.1 302 Found
Location: https://web-tls-rt.apps.ocp.lab/
Cache-Control: no-cache
None confuses people constantly: "the route works in the browser (which went to https) but curl says 503" - because curl asked for http.
Your own certificate on a Route
--cert is the certificate, --key its private key, --ca-cert the CA that signed it (so the router can send the whole chain):
oc create route edge shop --service=web --hostname=shop.lab \
--cert=shop.crt --key=shop.key --ca-cert=issuing-ca.crt
The PEMs are stored in the Route object (spec.tls.certificate, key, caCertificate) - anyone who can read the Route can read the key, so on shared projects prefer spec.tls.externalCertificate (a reference to a kubernetes.io/tls Secret, GA in recent 4.x) and keep Route read access tight.
passthrough
The router does not decrypt: it reads the SNI name in the TLS ClientHello, picks the route by host, and forwards the encrypted bytes to a pod. The pod presents its own certificate:
curl -kv https://payments-rt.apps.ocp.lab/ 2>&1 | grep -E 'subject:|issuer:'
* subject: CN=payments.rt.svc
* issuer: CN=openshift-service-serving-signer@1788984003
Consequences - all of them follow from "the router cannot see inside":
- the pod's certificate must be valid for the public hostname if clients verify it (this one is only valid for
payments.rt.svc, so outside clients need-kor a proper cert in the pod); - no paths (
spec.path: Invalid value: "/api": passthrough termination does not support paths), no cookies, no header rewrites, no router timeouts on HTTP; - the backend must speak TLS; point passthrough at a plain HTTP pod and the client gets garbage back:
curl https://web-pt-rt.apps.ocp.lab/
curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number
insecureEdgeTerminationPolicymay beNoneorRedirect, neverAllow.
Use passthrough when the application must own TLS end to end: mutual TLS (mTLS - the client presents a certificate too) with client certificates, a regulated service where the router must never see plaintext, non-HTTP TLS protocols.
reencrypt
The router terminates the client's TLS (with its default or the Route's certificate), then opens a new TLS connection to the pod and verifies the pod's certificate. You get the router's features (paths, cookies, timeouts, a trusted public cert) and encryption on the pod network.
How does the router trust the pod's certificate? Either you give it the CA in spec.tls.destinationCACertificate, or - leave it empty - the router trusts the cluster's service CA and checks that the cert is valid for <service>.<namespace>.svc. Which is exactly what the service CA hands out:
apiVersion: v1
kind: Service
metadata:
name: payments
annotations:
service.beta.openshift.io/serving-cert-secret-name: payments-tls
spec:
ports:
- name: https
port: 443
targetPort: 8443
oc get secret payments-tls
NAME TYPE DATA AGE
payments-tls kubernetes.io/tls 2 19s
oc get svc payments -o jsonpath='{.metadata.annotations.service\.beta\.openshift\.io/serving-cert-signed-by}{"\n"}'
openshift-service-serving-signer@1788984003
The service-ca operator noticed the annotation, issued a certificate for payments.rt.svc / payments.rt.svc.cluster.local, stored it in the Secret, and renews it before it expires. Mount the Secret in the pod, point the server at tls.crt/tls.key, and oc create route reencrypt needs no certificate flags at all. The same CA can be injected into ConfigMaps for clients inside the cluster (service.beta.openshift.io/inject-cabundle: "true" -> a service-ca.crt key; every project also gets an openshift-service-ca.crt ConfigMap).
The mismatches
| route | pod | client sees |
|---|---|---|
| edge | serves HTTP | works |
| edge | serves HTTPS | router speaks HTTP to a TLS port: Client sent an HTTP request to an HTTPS server. (400) |
| passthrough | serves HTTPS | works (pod's cert) |
| passthrough | serves HTTP | curl: (35) ... wrong version number |
| reencrypt | HTTPS, service-CA cert | works |
| reencrypt | HTTPS, self-signed, no destinationCACertificate | router cannot verify: 503 |
| reencrypt | serves HTTP | router's TLS handshake fails: 503 |
| any TLS route | - | http:// with policy None: 503 |
curl -k https://payments-edge-rt.apps.ocp.lab/
Client sent an HTTP request to an HTTPS server.
Choosing, in one line each:
- edge - the default for web apps and APIs: the router's cert, HTTP inside.
- reencrypt - policy says "encrypted in transit everywhere" and you still want the router's features. With the service CA it costs one annotation.
- passthrough - the app must terminate TLS itself (mTLS, strict key custody).
What you can now do
- Pick edge, passthrough or reencrypt for a service and say what each costs.
- Trust the cluster's ingress CA with
oc extract+curl --cacertinstead of-k. - Get a certificate from the service CA with one Service annotation, and recognise each TLS mismatch from what curl prints.