One public address, many services behind it — the ingress peeks at the hostname inside the TLS handshake and sends each connection to the right backend.
Dozens of services share a single IP and port 443. When a client connects, how does the front door know whether it wants the API, the store, or the docs site?
The client announces the hostname it's after in the very first TLS message, the ClientHello, in a field called SNI (Server Name Indication). The ingress reads SNI, looks it up in a routing table, picks the matching certificate, terminates TLS (decrypts), and forwards the plaintext request to that backend.
SNI travels in the ClientHello in the clear (it has to — the server needs it before a shared key exists), so the router can read it without decrypting anything. It matches the name against routes, selects that route's certificate, completes the handshake, decrypts, and proxies plaintext to the backend.
def on_connection(conn):
hello = read_client_hello(conn) # first TLS record, unencrypted
host = hello.sni # e.g. "api.shop.example"
route = routes.match(host) # exact, then wildcard *.shop.example
if route is None:
conn.close(alert="unrecognized_name") # no cert, no backend
return
tls = terminate(conn, route.certificate) # finish handshake, decrypt
backend = connect(route.upstream)
pipe(tls, backend) # forward plaintext both ways
| Step | Cost |
|---|---|
| SNI route lookup | O(1) exact / O(labels) wildcard |
| TLS termination | 1 handshake (CPU: asymmetric crypto) |
| Proxying | O(bytes) streamed |
The trade-off: terminating TLS lets you route on hostname and path and centralise certificates, but the ingress now sees plaintext and owns the private keys — it is a high-value target and a single point of failure.
unrecognized_name — don't leak the wrong site's certificate.*.shop.example is checked before api.shop.example, the exact host's dedicated route never wins.Host header (domain fronting). Re-check after termination if backends are sensitive.An ingress fronts three services on one IP. A browser opens store.shop.example: its ClientHello carries sni = store.shop.example. The router matches the store route, presents the store's certificate, finishes the handshake, and pipes the decrypted HTTP to the store pods. Moments later a bot connects to unknown.other.example; no route matches, so the ingress sends an unrecognized_name alert and closes — no backend is ever touched.
How can the router pick the right backend before decrypting anything?