Skip to content
TLS Termination
step 1/5

Reading — step 1 of 5

Read

~3 min readProduction Concerns

TLS Termination

HTTPS has to be decrypted somewhere. Where you put that boundary is an architecture decision, and the LB is the usual answer:

Client ---HTTPS---> Load Balancer ---HTTP---> Backend Pool
                    (decrypt here)

Terminate at the LB and you centralize the hard parts: one place to install and renew certificates, one place to enforce TLS versions and cipher policy, and — critically for this course — the LB can see the plaintext request, which is what makes every L7 feature (path routing, header rules, sticky cookies) possible. The costs: LB-to-backend traffic is cleartext (keep it on a private network, or re-encrypt), and the LB's private keys become your crown jewels.

The alternatives you'll meet in production: TLS passthrough (an L4 balancer forwards encrypted bytes untouched; backends terminate — no L7 features possible), and end-to-end with mTLS, where the LB terminates client TLS but speaks mutual TLS to backends — both sides present certificates. Service meshes (Istio, Linkerd) run mTLS between every pair of services as the basis of zero-trust networking.

SNI: many certificates, one IP

A TLS server must present its certificate before any HTTP bytes flow — so how does one IP serve example.com and other.io with different certs? SNI (Server Name Indication): the client puts the hostname it wants, in cleartext, inside the very first handshake message.

ClientHello: server_name = "api.example.com"
LB: look up cert for "api.example.com" -> present it -> handshake completes

Every LB therefore maintains exactly the data structure you're about to build: a table from hostname patterns to certificates, consulted once per handshake. (Cleartext SNI leaks which site you're visiting; Encrypted ClientHello (ECH) is the in-progress fix — good to know, out of scope here.)

Wildcard rules — the part with sharp edges

Certificates may cover *.example.com, but a wildcard matches exactly one label, only the left-most one (RFC 6125 and CA/Browser Forum rules):

  • *.example.com matches api.example.com
  • it does NOT match a.b.example.com (two labels) ✘
  • it does NOT match the bare apex example.com

That's why real deployments hold both an apex cert and a wildcard, and why "just get a wildcard" doesn't cover v1.api.example.com unless it's literally *.api.example.com. Match priority is: exact hostname first, then wildcard, then a default certificate (what nginx serves from its default_server block when nothing matches — clients then see a hostname-mismatch warning, but the handshake completes).

Your exercise

Implement lookup() in the starter's CertTable. CERT <pattern> <name> registers (or replaces) a pattern; LOOKUP <hostname> resolves: exact match first, else the one-level wildcard *.<everything after the first label>, else the literal string default. LIST prints <pattern> -> <name> sorted by pattern (note * sorts before letters, so wildcards list first).

Grader traps, all from real tests:

  • One level only. With *.example.com registered, LOOKUP a.example.comwild but LOOKUP a.b.example.comdefault. If you match any suffix, you fail.
  • Wildcards don't cover the apex. With only *.api.example.com registered, LOOKUP v1.api.example.comapi_wild, but LOOKUP api.example.comdefault.
  • Exact beats wildcard. With both *.example.com -> wild and api.example.com -> exact, LOOKUP api.example.comexact, and web.example.com still gets wild.
  • Re-registration replaces. CERT a.com first then CERT a.com second: lookups return second and LIST shows one line, a.com -> second.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…