OnCallReady

Lesson 30.15 · OpenShift · 20 min read

Route TLS: edge, passthrough, reencrypt

In plain words

Imagine sending a locked box through a post office. Option one: the post office unlocks it at the counter, carries the letter openly to your house (edge). Option two: the post office never opens it; it reads only the name on the outside and delivers the locked box to you, and only you have the key (passthrough). Option three: the post office opens it at the counter, checks it, then locks it again in a new box that only you can open for the last stretch (reencrypt).

Those are the three Route TLS modes. Edge: the router terminates TLS and talks HTTP to the pod. Passthrough: the router reads only the SNI name (the host name the client sends at the start of TLS) and the pod presents its own certificate. Reencrypt: the router terminates, then opens a new TLS connection, trusting pods whose certificates come from the cluster's service CA (its own certificate authority), switched on with a Service annotation.

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

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:

policycurl http://host/
unset / None (default)503 "Application is not available" - there is no HTTP route for that host
Redirect302 Found, Location: https://host/...
Allowserved 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":

curl https://web-pt-rt.apps.ocp.lab/
curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number

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

routepodclient sees
edgeserves HTTPworks
edgeserves HTTPSrouter speaks HTTP to a TLS port: Client sent an HTTP request to an HTTPS server. (400)
passthroughserves HTTPSworks (pod's cert)
passthroughserves HTTPcurl: (35) ... wrong version number
reencryptHTTPS, service-CA certworks
reencryptHTTPS, self-signed, no destinationCACertificaterouter cannot verify: 503
reencryptserves HTTProuter'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:

What you can now do

Why it helps

Security policies in banks often say "encrypted in transit everywhere" or "the platform must never see plaintext for this service", and choosing between reencrypt and passthrough is how you meet them. With the service CA, reencrypt costs one Service annotation: the service-ca operator issues and renews a certificate for payments.rt.svc, and the router trusts it automatically.

The mismatches produce confusing errors you will debug: an edge route to a pod that speaks HTTPS returns Client sent an HTTP request to an HTTPS server; passthrough to a plain HTTP pod gives wrong version number; reencrypt to a self-signed pod gives a 503. "Works in the browser, curl says 503" is the insecure policy None. And private keys stored inline in Route objects readable by the whole team are a finding in any security review.

Commands in this lesson

curl

FAQ

When should I use edge, passthrough or reencrypt?

Edge is the default for web apps and APIs: the router presents a trusted certificate and talks HTTP inside the cluster, with all router features. Reencrypt adds TLS between router and pod, keeping paths, cookies and timeouts, for "encrypt in transit everywhere" policies. Passthrough is for apps that must own TLS end to end: mutual TLS with client certificates, strict key custody, or non-HTTP TLS protocols.

Why does curl http:// return 503 while the browser works?

The route is a TLS route with insecureEdgeTerminationPolicy unset or None, so the router serves nothing on plain HTTP for that host and returns its 503 page. The browser went to https. Set the policy to Redirect to send a 302 to https, or Allow to serve plain HTTP as well (edge and reencrypt only). Passthrough routes support None or Redirect.

What is the service CA?

A cluster-internal certificate authority run by the service-ca operator. Annotating a Service with service.beta.openshift.io/serving-cert-secret-name: payments-tls makes it issue a certificate valid for payments.<namespace>.svc, store it in that Secret and renew it automatically. Reencrypt routes trust it by default, and clients inside the cluster can get the CA bundle injected into a ConfigMap with service.beta.openshift.io/inject-cabundle: "true".

Why is curl complaining about a self-signed certificate on my edge route?

Without a certificate on the Route, the router presents its default wildcard certificate for *.apps.<domain>, which on a fresh cluster is signed by the Ingress Operator's own CA. Your machine does not trust it. Extract the CA with oc extract configmap/default-ingress-cert -n openshift-config-managed and use --cacert, rather than -k. Production clusters replace the default certificate with a company or public wildcard.

Is it safe to put my certificate and key in the Route?

The PEM certificate, key and CA are stored inline in the Route object, so anyone who can read the Route can read the private key, which in many projects includes all developers with view rights. On shared projects prefer spec.tls.externalCertificate, a reference to a kubernetes.io/tls Secret, so normal Secret RBAC protects the key, and keep Route read access tight.

In an interview Mid

Explain the three TLS termination types for OpenShift Routes and when you use each.

The question is where TLS ends:

Common mismatches: edge to an HTTPS pod gives Client sent an HTTP request to an HTTPS server; reencrypt to an HTTP or untrusted pod gives a 503. And trust the cluster's ingress CA with oc extract + curl --cacert, not -k.

Also asked: How would you implement end-to-end encryption for a service on OpenShift with minimal effort? · What does the service CA do? · Why does curl get a 503 on http:// while the browser works on https://?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.