2.8
TLS 1.2 vs TLS 1.3 Handshakes, and Mutual TLS
Every message of both handshakes, forward secrecy, 0-RTT, and services proving their identity to each other.
45 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Walk through the TLS 1.2 handshake message by message (ClientHello to Finished) and say what each message proves.
- Explain why RSA key transport was removed and what forward secrecy protects against.
- Walk through the TLS 1.3 handshake: key share in the first message, the HKDF key schedule, what is encrypted, HelloRetryRequest, the five cipher suites and the features that were removed.
- Count the round trips of TCP + TLS 1.2, TCP + TLS 1.3, QUIC, resumption and 0-RTT, and explain why 0-RTT is only safe for idempotent requests.
- Explain SNI, Encrypted Client Hello, ALPN and the post-quantum hybrid key share, and recognise the common handshake failures from their symptoms.
- Choose between TLS termination, re-encryption and passthrough at a load balancer.
- Explain mutual TLS end to end: CertificateRequest, client certificates, SPIFFE identities, short-lived certificates and rotation, sidecar vs ambient meshes, and when to use mTLS instead of API keys or JWTs.
#Where this lesson fits
Lesson 1.9 gave you the overview: TLS gives confidentiality, integrity and authentication; TLS 1.3 takes one round trip; browsers check the certificate’s name, dates and chain; 0-RTT can be replayed; HSTS forces HTTPS. Lesson 2.7 taught the building blocks: symmetric and asymmetric keys, ECDHE, signatures, certificates, CAs and the chain of trust. Lesson 1.12 introduced mTLS as part of zero trust.
This lesson puts those pieces together and opens the handshake itself. A TLS handshake is the process by which a client and a server agree on cryptographic algorithms, verify identity, and create a secure symmetric session key for encryption. Every handshake, in every version, has to achieve the same four things:
| Goal | Why it matters | TLS 1.2 does it with | TLS 1.3 does it with |
|---|---|---|---|
| Agree on a version and algorithms | Both sides must speak the same cipher | ClientHello list → ServerHello choice | Same, plus supported_versions |
| Agree on keys nobody else knows | All data is encrypted with them | ServerKeyExchange + ClientKeyExchange (ECDHE) | key_share in both hellos |
| Authenticate the server (and optionally the client) | Otherwise you might be talking to an attacker | Certificate + signature in ServerKeyExchange | Certificate + CertificateVerify |
| Prove nobody tampered with the handshake | An attacker could delete strong options from the hellos | Finished (keyed hash of all messages) | Finished, with every key bound to the transcript |
The two versions reach the same goals; TLS 1.3 just gets there one round trip sooner and hides much more on the way.
#TLS 1.2, message by message
TLS 1.2 (RFC 5246, 2008) needs two round trips after TCP before application data flows. The notes draw it as four arrows; here are all the real messages.
The TLS 1.2 handshake, message by message
TCP is already established. The recorder in the middle copies every byte, hoping to decrypt it some day.
Scenario
The normal TLS 1.2 handshake with an ephemeral elliptic-curve key exchange and an RSA certificate (TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256).
The client sends its highest version (1.2), a 32-byte client random, a list of cipher suites in order of preference, and extensions: SNI (shop.example.com), ALPN (h2, http/1.1), supported groups (x25519, P-256) and signature algorithms.
1. ClientHello (client → server)
The client sends the highest version it supports, a 32-byte client random, its cipher suites in order of preference, and extensions: SNI (the hostname), ALPN (
h2,http/1.1), the elliptic-curve groups it supports and the signature algorithms it accepts.2. ServerHello, Certificate, ServerKeyExchange, ServerHelloDone (server → client)
One flight of four messages. ServerHello picks one cipher suite and adds a 32-byte server random. Certificate is the chain, leaf first, sent unencrypted. ServerKeyExchange holds the server’s fresh, ephemeral ECDHE public key, signed with the certificate’s private key over both randoms, which proves the key comes from the certificate’s owner and belongs to this session. ServerHelloDone says “your move”. The server cannot send Finished yet: it has not seen the client’s key share.
3. The client verifies and computes
The client checks the chain, the hostname and the dates (lesson 2.7), makes its own ephemeral key pair and computes the shared ECDHE value, the pre-master secret. A pseudorandom function (PRF) mixes it with both randoms into a 48-byte master secret, which is then expanded into a key block: separate encryption keys (and initialisation vectors) for each direction.
4. ClientKeyExchange, ChangeCipherSpec, Finished (client → server)
ClientKeyExchange carries the client’s ephemeral public key, so the server can compute the same secret. ChangeCipherSpec means “from my next record on, I encrypt”. Finished is the first encrypted message: a keyed hash of every handshake message so far.
5. ChangeCipherSpec, Finished (server → client)
The server checks the client’s Finished and sends its own. If anyone had altered the hellos, for example deleting the strong cipher suites to force a weak one, the two sides would compute different hashes and the handshake would fail. Now application data flows.
Cost: 2 round trips for TLS on top of 1 for TCP. Browsers used False Start to send their first request right after their own Finished (saving one round trip with ECDHE suites), but the protocol count is two.
#RSA key transport vs ECDHE: forward secrecy
TLS 1.2 also allowed a simpler key exchange, RSA key transport: the client invents the pre-master secret and sends it encrypted with the server’s RSA public key from the certificate. There is no ServerKeyExchange. It works, and it was the default for years. Play the second scenario above to see the problem.
Everything depends on one long-lived key. An attacker who records the encrypted traffic today and steals the private key later (a breach, a stolen backup, a memory-disclosure bug like Heartbleed in 2014, a court order) can decrypt every recorded ClientKeyExchange, recover each session’s secret, and read years of traffic. With ECDHE, each handshake uses fresh key pairs that are deleted afterwards; the certificate key only signs. Stealing it later lets an attacker impersonate the server in the future, but not decrypt the past. That property is forward secrecy.
RSA key transport also suffered padding-oracle attacks (Bleichenbacher in 1998, revived in 2017). So TLS 1.3 removed it: every TLS 1.3 handshake uses ephemeral Diffie-Hellman.
#Reading a TLS 1.2 cipher suite
A TLS 1.2 suite name packs four choices into one string:
Part of TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | Means |
|---|---|
ECDHE | Key exchange: ephemeral elliptic-curve Diffie-Hellman (forward secrecy) |
RSA | Authentication: the certificate holds an RSA key, which signs the ECDHE parameters |
AES_128_GCM | Bulk cipher: AES with a 128-bit key in GCM, an AEAD mode (encrypts and authenticates together) |
SHA256 | Hash for the PRF that derives keys |
Hundreds of such combinations were registered, many of them weak (export-grade, RC4, 3DES, CBC with a separate MAC). A big part of hardening a TLS 1.2 server is deleting most of the list.
#TLS 1.3: one round trip, mostly encrypted
TLS 1.3 (RFC 8446, 2018) removes unnecessary steps and completes the handshake in one round trip (1-RTT), which lowers latency significantly. The trick is in the first message: the client guesses which key-exchange group the server will accept and sends its key share right away.
The TLS 1.3 handshake: one round trip, mostly encrypted
Messages in {braces} are encrypted with handshake keys; [brackets] mean application data. TCP is already established.
Scenario
One message carries everything: supported_versions (1.3, 1.2), three cipher suites, a key_share for X25519MLKEM768 and one for X25519, SNI, ALPN and signature algorithms. Sending the key share now is what saves the round trip.
1. ClientHello + key_share (client → server)
supported_versionslists 1.3 and 1.2 (the old version field is frozen at 1.2 so that broken middleboxes let it through), three or so cipher suites, a key_share for each group it bets on (browsers today send X25519MLKEM768 and X25519), SNI, ALPN and signature algorithms.2. ServerHello + key_share (server → client)
The server picks the version, the suite and one group, and returns its own share. Both sides can now compute the shared secret. This is the last plaintext handshake message.
3. {EncryptedExtensions}, {CertificateRequest}, {Certificate}, {CertificateVerify}, {Finished} (server → client)
Same flight, encrypted with handshake keys. EncryptedExtensions carries answers that need not be public (such as the chosen ALPN protocol). CertificateRequest appears only in mutual TLS. Certificate is now encrypted, so an observer cannot see which certificate was sent. CertificateVerify is a signature over the hash of the whole handshake so far, proving the server holds the private key for this exact session. Finished is a keyed hash over the transcript. The server may even start sending data now (“0.5-RTT” data).
4. {Finished} + first request (client → server)
The client verifies everything, sends Finished, and its first HTTP request in the same flight. After the handshake the server sends one or more NewSessionTicket messages for resumption next time.
#The key schedule: HKDF
TLS 1.2 derived keys once from the master secret. TLS 1.3 uses HKDF (HMAC-based key derivation) in a chain of stages, and every stage mixes in a hash of the handshake messages so far (the transcript):
PSK (or zeros) ──► Early Secret ──► client_early_traffic_secret (0-RTT data)
│
ECDHE shared ──────► Handshake Secret ──► client/server handshake traffic secrets
│ (EncryptedExtensions … Finished)
▼
Master Secret ──► client/server application traffic secrets
──► resumption secret (→ tickets for next time)
──► exporter secret (for protocols built on TLS)
Three consequences worth knowing for interviews. Keys are separate per direction and per phase, so a bug in one phase does not expose another. Because every key depends on the transcript, any tampering changes every key and the handshake simply fails. And a long connection can roll its keys forward with a KeyUpdate message without a new handshake.
#HelloRetryRequest: when the guess is wrong
If the client sent shares only for groups the server will not use, but listed one it does support, the server replies with a HelloRetryRequest (a ServerHello with a special fixed random value): “send me a share for P-256”. The client sends a second ClientHello, identical apart from the new share, and the handshake continues. That costs one extra round trip. Play the second scenario above.
This is why clients send two shares, and why a server should support what browsers guess first (today X25519MLKEM768 and X25519). A HelloRetryRequest can also carry a cookie, letting a server under a flood stay stateless until the client proves it can receive packets at its address.
#Cipher suites in TLS 1.3
In TLS 1.3 the suite names only the AEAD cipher and the hash. Key exchange is negotiated separately (supported_groups, key_share) and so are signatures (signature_algorithms). Only five suites exist:
| Suite | Notes |
|---|---|
TLS_AES_128_GCM_SHA256 | Mandatory; the most used |
TLS_AES_256_GCM_SHA384 | Longer key; often required by government profiles |
TLS_CHACHA20_POLY1305_SHA256 | Fast in software; preferred on phones without AES hardware |
TLS_AES_128_CCM_SHA256 | For constrained IoT devices |
TLS_AES_128_CCM_8_SHA256 | Short tag for IoT; marked “not recommended” |
There is nothing weak left to misconfigure: every suite is AEAD, and every key exchange is ephemeral.
#What TLS 1.3 removed, and why
| Removed | Why |
|---|---|
| RSA key transport and static Diffie-Hellman | No forward secrecy; padding-oracle attacks |
| CBC-mode ciphers with a separate MAC | Padding-oracle and timing attacks (Lucky 13 and others) |
| RC4, 3DES, export ciphers, MD5 and SHA-1 signatures | Broken or too weak |
| Compression | Leaked secrets through response sizes (the CRIME attack) |
| Renegotiation | A source of attacks and complexity; replaced by KeyUpdate and post-handshake authentication |
| Custom Diffie-Hellman groups | Servers chose weak groups; now only named, vetted groups |
| ChangeCipherSpec (as a real message) | Key changes are implicit; a dummy copy is sent only to keep old middleboxes calm |
| Session IDs and tickets as separate mechanisms | Unified into pre-shared keys (PSK) |
Two more protections are worth naming. Downgrade protection: a 1.3-capable server that ends up speaking 1.2 writes a fixed marker into the last 8 bytes of its random; a 1.3 client that sees it knows an attacker stripped 1.3 from the hello, and aborts. Middlebox compatibility: a TLS 1.3 handshake is dressed up to look like a TLS 1.2 session resumption, because a measurable share of firewalls broke on anything new. TLS 1.0 and 1.1 are formally deprecated (RFC 8996, 2021); most browser connections today use TLS 1.3, and TLS 1.2 survives in older clients, IoT and enterprise systems.
Self-checkQuestion
In a TLS 1.3 handshake, which is the last message an observer on the network can read in clear?
#Counting round trips
The round-trip race: two handshakes side by side
Both lanes start at the same moment on the same 100 ms path. Each step is half a round trip (50 ms). Watch when each lane’s request can finally leave.
Scenario
Lane A: TCP + TLS 1.2. Lane B: TCP + TLS 1.3. Brand-new connections, nothing cached.
Race clock (100 ms round trip)
| Lane | Round trips used | Clock |
|---|---|---|
| A: TLS 1.2 | 0 | 0 → 50 ms |
| B: TLS 1.3 | 0 | 0 → 50 ms |
Both clients start a TCP handshake. TLS needs an established byte stream first, so both lanes pay this round trip whatever TLS version they speak.
The race shows the same thing as the table below. With a 100 ms round trip, a brand-new connection costs:
| Setup | Round trips before the request | Response at (100 ms RTT) | At 200 ms RTT |
|---|---|---|---|
| TCP + TLS 1.2 | 3 | 400 ms | 800 ms |
| TCP + TLS 1.3 | 2 | 300 ms | 600 ms |
| TCP + TLS 1.3, HelloRetryRequest | 3 | 400 ms | 800 ms |
| TCP + TLS 1.3, PSK resumption | 2 (no certificate, cheaper) | 300 ms | 600 ms |
| TCP + TLS 1.3, 0-RTT | 1 (TCP only) | 200 ms | 400 ms |
| QUIC (TLS 1.3 inside) | 1 | 200 ms | 400 ms |
| QUIC with 0-RTT | 0 | 100 ms | 200 ms |
| Reused connection | 0 | 100 ms | 200 ms |
“Response at” includes one round trip for the request itself. DNS and any redirect come on top.
What else costs time. CPU is rarely the bottleneck now: an X25519 key exchange takes tens of microseconds, an ECDSA P-256 signature a few tens of microseconds, an RSA-2048 signature roughly a millisecond (the main reason busy servers prefer ECDSA certificates). Size matters more: a certificate chain of 3–5 KB plus a 1–2 KB post-quantum key share must fit in TCP’s first congestion window (about 14 KB); a bloated chain or RSA-4096 keys can push the server’s flight past it and add a round trip. Certificate compression (RFC 8879) helps. The biggest lever is not handshaking at all: keep-alive, connection pools, resumption, and terminating TLS at an edge close to the user, so the round trip itself is 10 ms instead of 200.
#Session resumption and 0-RTT
Resumption and 0-RTT early data, and why replay matters
The client holds a ticket from yesterday. The attacker can record and resend packets but cannot decrypt them.
Scenario
The ticket is offered as a pre-shared key, with a “binder” (an HMAC proving the client really holds the secret) and a fresh key share. Browsers use psk_dhe_ke mode: PSK plus a new ECDHE exchange, which keeps forward secrecy.
TLS 1.2 had two ways to resume. With a session ID, the server keeps a cache of recent sessions; behind a load balancer the client often lands on a server that does not have it. With a session ticket (RFC 5077), the server encrypts the session state with a ticket key and gives it to the client, so it stores nothing; every server in the fleet must share the ticket key. An abbreviated handshake takes 1 round trip and skips the certificate. The weak spot: a resumed TLS 1.2 session has no fresh key exchange, so a stolen ticket key decrypts every recorded session resumed under it. Rotate ticket keys every few hours and keep them in memory only.
TLS 1.3 unifies both into a pre-shared key (PSK). The NewSessionTicket from last time is offered in pre_shared_key, with a binder (an HMAC proving the client holds the secret). In psk_dhe_ke mode, which browsers use, a fresh ECDHE exchange is added, so resumption keeps forward secrecy. Tickets live at most 7 days.
0-RTT early data goes one step further: for returning clients, the client sends encrypted data in the very first flight, before the handshake completes. The early data is encrypted with a key derived from the ticket alone. That has two costs:
- Replay. Nothing in the first flight comes from the server, so an attacker can record it and send it again, to the same server or another edge server, and it will decrypt correctly. TLS can limit this (single-use tickets, a recent-ClientHello cache, a short freshness window) but cannot rule it out across a distributed fleet.
- No forward secrecy for the early data itself: it depends only on the ticket’s secret.
So the rule is: accept 0-RTT only for safe, idempotent requests (GET of a page, a price list, static files). For anything that changes state, the server refuses the early data, answers 425 Too Early (RFC 8470), or forwards it with an Early-Data: 1 header so the origin decides; the client then resends after the handshake. nginx ships with ssl_early_data off; CDNs that offer 0-RTT usually limit it to GET requests.
#Extensions that shape the handshake: SNI, ECH and ALPN
SNI, Encrypted Client Hello and ALPN: what the first message decides
One IP address, 203.0.113.10, serves shop.example.com and blog.example.org, each with its own certificate.
Scenario
The hostname goes in the server_name extension of the ClientHello, because the HTTP Host header will only be sent after the handshake, encrypted.
SNI (Server Name Indication). One IP address can serve thousands of HTTPS sites, but the HTTP Host header arrives only after the handshake. So the client puts the hostname in the server_name extension of the ClientHello, and the server picks the matching certificate. If the client sends no SNI (some old HTTP libraries, health checks that connect by IP), the server can only present its default certificate, and the client fails with a hostname mismatch; a strict server sends an unrecognized_name alert instead. SNI is plaintext, so anyone on the path learns which site you visit, even with TLS 1.3.
ECH (Encrypted Client Hello). The site publishes an ECH public key in its DNS HTTPS record (lesson 1.8). The client encrypts its real ClientHello (with the real SNI and ALPN) and wraps it in an outer ClientHello whose SNI is a shared public name of the provider. Chrome and Firefox have supported it since 2023 and large CDNs offer it. It only hides the site when many sites share the provider’s addresses; on a dedicated IP, the address gives the site away. Some networks block ECH to keep filtering by name.
ALPN (Application-Layer Protocol Negotiation). The client offers protocols (h2, http/1.1; h3 over QUIC) and the server picks one inside the handshake, so no extra round trip is spent. If there is no overlap, a strict server answers no_application_protocol. The classic case is gRPC, which needs HTTP/2, behind a proxy that only speaks HTTP/1.1.
#Post-quantum key exchange
A future large quantum computer could break X25519 and RSA. Signatures only matter at handshake time, but key exchange matters for as long as the data stays secret: an attacker can harvest now, decrypt later. So browsers moved first on key exchange.
The group X25519MLKEM768 combines classic X25519 with ML-KEM-768, the lattice-based key-encapsulation mechanism NIST standardised in 2024 (FIPS 203). The shared secret depends on both, so the connection is safe if either holds. Chrome made it the default in 2024, Firefox followed, Apple added it in its 2025 operating systems, and OpenSSL 3.5 (2025) offers it by default; large CDNs already negotiate it for a large share of browser traffic.
The price is size: the client’s ML-KEM share alone is 1184 bytes, and the server’s answer 1088, so a ClientHello grows to about 1.8 KB and needs two TCP segments. Some old middleboxes that expected the hello in one packet dropped it, which shows up as a hang, not a TLS error (third scenario of the failure animation below). Certificates and their signatures are still classic for now; post-quantum signatures are larger and still being rolled out.
#Where TLS ends: termination, re-encryption, passthrough
Where TLS ends: termination, re-encryption and passthrough
The same client and backends, three ways to place the load balancer. The table shows what the load balancer can see.
Scenario
The load balancer holds the certificate and decrypts. Backends receive plain HTTP.
What the load balancer sees
SNI
shop.example.com
Method + path
POST /api/cart
Headers, cookies
all readable
The client’s TLS handshake ends at the load balancer, which holds the certificate and private key for shop.example.com.
| Mode | Where the certificate lives | What the load balancer can do | Inside traffic | Typical use |
|---|---|---|---|---|
| Termination (offload) | Load balancer | Everything layer 7: route by path and host, add headers, WAF, caching, compression | Plain HTTP | Most web apps behind an AWS ALB, nginx or a CDN |
| Re-encryption (bridging) | Public cert at the load balancer, internal certs on backends | Everything layer 7 | Encrypted with a second TLS session | Compliance (card or health data), zero trust |
| Passthrough | Backends only | Route by SNI or port only; no paths, headers, WAF or cache | Encrypted end to end | End-to-end client mTLS, a load balancer you may not trust with keys, non-HTTP protocols |
How to choose. Start with termination at the edge: it centralises certificates, offloads crypto and makes routing flexible. Add re-encryption when the network inside must be encrypted too, which today is the default expectation. Use passthrough only when the backend itself must see the client’s TLS (end-to-end mTLS) or the load balancer must never hold the keys; you lose layer-7 features and must carry the client IP with the proxy protocol. On AWS, ALB terminates (and can re-encrypt to HTTPS targets); NLB can terminate with a TLS listener or pass through with a TCP listener.
When client mTLS is verified at the load balancer, the backend cannot see the certificate, so the load balancer forwards the verified identity in a header (x-forwarded-client-cert in Envoy, X-Amzn-Mtls-Clientcert on AWS ALB). The load balancer must strip any copy sent by the client, and backends must accept traffic only from the load balancer, or anyone who reaches a backend can claim any identity.
#HSTS and the preload list, briefly
Lesson 1.9 explained HSTS: the Strict-Transport-Security header makes the browser use HTTPS for the site on its own, and turns certificate errors into hard failures. The first visit is still exposed, which the preload list fixes. To be accepted at hstspreload.org a site must serve a valid certificate, redirect HTTP to HTTPS on the same host, and send max-age of at least one year with includeSubDomains and preload. Removal takes months to reach users, so roll it out with a short max-age first. For handshakes, HSTS matters because it removes the plain-HTTP redirect, one more round trip saved before the first secure byte.
#When the handshake fails
When the handshake fails in production
Three failures that do not show up in a quick browser test. The middle actor is whatever sits on the path: a firewall, a proxy, a load balancer.
Scenario
After a certificate renewal, the server is configured with the leaf certificate only, not the full chain.
A normal handshake starts.
Handshake failures end with a TLS alert, and its name usually tells you where to look:
| Symptom (alert or error) | Usual cause | Fix |
|---|---|---|
certificate_expired (45), NET::ERR_CERT_DATE_INVALID | Renewal failed, or the client’s clock is wrong | Automate renewal and alert well before expiry; sync time with NTP |
| “not yet valid” on one device type | Device booted with a wrong clock (no battery-backed clock) | Sync time before the first TLS call |
unable to get local issuer certificate in curl, Java, apps; browser fine | Server sends only the leaf, not the intermediate | Deploy the full chain; test with openssl s_client |
| Hostname mismatch only from some clients | SNI missing, so the default certificate is served | Make the client send SNI; set the expected name for checks by IP |
protocol_version (70) | Client too old (TLS 1.0/1.1) or server minimum too high | Upgrade the client; keep the server at TLS 1.2 minimum |
handshake_failure (40), “no shared cipher” | No common cipher suite or group | Fix the client; keep weak suites off the main endpoint |
no_application_protocol (120) | ALPN mismatch, for example gRPC (h2) behind an HTTP/1.1-only proxy | Enable h2 on the proxy or load balancer |
unknown_ca (48) | Certificate from a CA the peer does not trust (wrong environment, private CA not installed) | Install the right CA bundle; never “fix” it by turning verification off |
certificate_required (116) | Server requires a client certificate and none was sent (mTLS) | Mount the client certificate; check the client library sends it |
| Hang or reset after ClientHello, no alert | A middlebox drops the large post-quantum ClientHello, or MTU problems | Update the middlebox; check MTU and fragmentation |
#Mutual TLS (mTLS)
In ordinary TLS/HTTPS only the client verifies the server (one-way authentication): the server proves it is shop.example.com, while the client stays anonymous at the TLS level and logs in later with a password or token. In mutual TLS (mTLS) both the server and the client verify each other’s certificates (two-way authentication).
[ One-way TLS / standard HTTPS ]
Client ── verifies server certificate ──► Server
[ Mutual TLS (mTLS) ]
Client ── verifies server certificate ──► Server
Client ◄── verifies client certificate ── Server
Mutual TLS: both sides show a certificate
The orders service calls the payments service. Both hold short-lived certificates from the company’s private CA.
Scenario
An ordinary TLS 1.3 ClientHello. Nothing in it says “I have a certificate”: the server decides whether to ask.
#How it works
1. The server sends its certificate, as in normal TLS
The handshake starts exactly like a normal one.
2. The server requests a client certificate (CertificateRequest)
As an extra step, the server sends CertificateRequest, listing the signature algorithms and, optionally, the CAs it accepts.
3. The client sends its certificate and a proof signed with its private key
The client sends its own Certificate and a CertificateVerify: a signature over the handshake transcript made with its private key. The key never leaves the client, only the proof that it holds it. In TLS 1.3 both are encrypted; in TLS 1.2 the client certificate travelled in clear, revealing who the client was.
4. The server verifies, and only then accepts the connection
The server checks the chain against its client CA bundle, the dates and the signature. If any check fails, it ends the handshake with an alert:
certificate_required(no certificate),unknown_ca(wrong CA),certificate_expired, orbad_certificate. Then comes a separate step that people forget: authorisation. A valid certificate says who is calling; a policy must still decide what that caller may do (last scenario above).
#Use cases
- Microservices. Securing internal service-to-service traffic, usually done automatically by a Kubernetes service mesh such as Istio or Linkerd.
- Zero trust. Proving the identity of every workload and device, instead of trusting anything because it is “inside” the network (lesson 1.12; zero-trust architecture is Chapter 33).
- Financial and B2B APIs. Banks, payment gateways and open-banking APIs often require partners to present client certificates, sometimes binding OAuth tokens to them (OAuth is Chapter 34–35).
- Devices. IoT devices authenticate to their cloud with a certificate provisioned at the factory.
#Service identity: SPIFFE and SPIRE
A certificate for a service needs a name that means something to your policies. SPIFFE is an open standard for that name: a URI like spiffe://example.com/ns/shop/sa/orders (trust domain, then a path; Istio uses namespace and service account). It is carried in an SVID, usually an X.509 certificate with the ID in its URI SAN. SPIRE is the reference implementation: a server that signs identities and an agent on every node that attests a workload (which Kubernetes service account, which node, which process) before handing it a certificate over a local socket. Istio and Linkerd issue SPIFFE-format identities from their own CAs.
#Short-lived certificates and rotation
Internal mTLS certificates are deliberately short-lived: one hour by default in SPIRE, 24 hours in Istio and Linkerd. A stolen key is then useful for hours, not a year, and revocation lists become almost unnecessary. The price is that renewal becomes a critical path: proxies renew at around half the lifetime, and if the CA is unreachable for longer than the remaining lifetime, every call fails at once (last scenario of the mesh animation). Run the CA highly available, alert on renewal failures and certificate age, and pick a lifetime longer than the worst CA outage you can repair.
Use a private CA for client certificates. Public CAs vouch for domain names, not for your services, and during 2026 public web CAs stop issuing certificates with the client-authentication EKU at all (a Chrome root-program rule), so mTLS built on public web certificates breaks.
#Service mesh: sidecars vs ambient
mTLS in a service mesh: identities, proxies and rotation
The applications speak plain HTTP to a local proxy; the proxies do mTLS with certificates from the mesh CA.
Scenario
Each pod has its own proxy container (Envoy in Istio, linkerd2-proxy in Linkerd).
At start-up each proxy proves to the CA which workload it runs as (Kubernetes service account, node attestation) and receives a short-lived certificate whose URI SAN is its SPIFFE ID. The private key is generated locally and never leaves the pod.
In a sidecar mesh every pod gets its own proxy (Envoy in Istio, linkerd2-proxy in Linkerd). The application speaks plain HTTP to localhost; the proxies do mTLS, enforce policy, retry and collect metrics. The application needs no TLS code, but every pod pays for a proxy in memory and CPU, there is an extra hop on each side, and upgrading the proxy restarts pods.
In ambient (sidecarless) mode, generally available in Istio since late 2024, one shared proxy per node (ztunnel) carries traffic in mTLS tunnels with each pod’s own identity and enforces layer-4 policy; an optional waypoint proxy adds layer-7 rules only where needed. Cilium offers a similar node-level model. Same guarantee, far fewer proxies. Mesh architecture and its trade-offs are covered in Chapters 33 and 46.
#mTLS vs API keys vs JWT
| API key | JWT (bearer token) | mTLS | |
|---|---|---|---|
| What is sent | A secret string in a header | A signed token with claims and an expiry | Only a proof of holding a private key |
| If intercepted or logged | Usable until rotated | Usable until it expires | Useless without the private key |
| Identifies | An application or account | A user or service, with claims (roles, scopes) | A workload or device (certificate subject, SPIFFE ID) |
| Layer | Application (HTTP) | Application (HTTP) | Transport (TLS) |
| Survives a TLS-terminating proxy | Yes | Yes | No: the proxy must forward the identity |
| Operations | Easy to issue; hard to rotate everywhere | Needs a signing key and an issuer | Needs a CA, issuance, rotation, trust bundles |
| Best for | Simple public APIs, metering | End-user identity across services | Service-to-service, partners, devices |
They are not rivals. A common production design uses mTLS for workload identity (which service is calling) and a JWT for the end user on whose behalf it calls; OAuth can even bind tokens to the client certificate so a stolen token is useless elsewhere (Chapter 34–35).
#TLS in system design
- Prefer TLS 1.3; keep TLS 1.2 with ECDHE and AEAD suites only for older clients; disable 1.0 and 1.1.
- Reuse connections (keep-alive, pools, HTTP/2 and HTTP/3). The fastest handshake is the one you do not do.
- Terminate at the edge, close to users, and keep warm, re-encrypted connections from the edge to your origin.
- Resumption on, ticket keys rotated and shared across the fleet; 0-RTT only for idempotent paths, if at all.
- ECDSA certificates (or both ECDSA and RSA) to cut handshake CPU; keep chains short and complete.
- Support X25519MLKEM768 on your edge; test that your middleboxes pass a two-packet ClientHello.
- Automate certificates and watch their age; most TLS outages are expiry, a missing intermediate, or a clock.
- Inside: mTLS with short-lived, automatically rotated certificates from a private CA, plus authorisation policy per service.
#Try it yourself
# Full TLS 1.3 handshake details: version, cipher, group, certificate chain
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
# Force TLS 1.2 and see the classic cipher suite name
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -brief </dev/null
# Ask for the post-quantum hybrid group (OpenSSL 3.5 or newer)
openssl s_client -connect example.com:443 -servername example.com -groups X25519MLKEM768 -brief </dev/null
# Missing SNI: see which certificate a shared server returns by default
openssl s_client -connect example.com:443 -noservername </dev/null | openssl x509 -noout -subject
# Show the whole chain the server sends (count the certificates)
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
# Resumption: save a session, then reuse it ("Reused, TLSv1.3")
openssl s_client -connect example.com:443 -servername example.com -sess_out /tmp/s.pem </dev/null
openssl s_client -connect example.com:443 -servername example.com -sess_in /tmp/s.pem </dev/null | grep -i reused
# Time each phase: TCP connect vs TLS handshake
curl -so /dev/null -w "tcp %{time_connect} tls %{time_appconnect}\n" https://example.com
# Mutual TLS from the client side
curl --cert client.crt --key client.key --cacert internal-ca.pem https://api.internal.example.com/health
In Wireshark, filter tls.handshake.type == 1 for ClientHellos and look at server_name, supported_versions, key_share (an X25519MLKEM768 share is over 1200 bytes). Compare a TLS 1.2 capture, where you can read the Certificate message, with a TLS 1.3 capture, where everything after ServerHello is “Application Data”.
#Summary
The notes’ summary rows for this lesson, with the details added:
| Topic | Summary |
|---|---|
| TLS 1.2 vs 1.3 | TLS 1.2 takes 2 RTT (ClientHello; ServerHello, Certificate, ServerKeyExchange, ServerHelloDone; ClientKeyExchange, ChangeCipherSpec, Finished; ChangeCipherSpec, Finished). TLS 1.3 takes 1 RTT (key share in ClientHello, everything after ServerHello encrypted), and resumption can use 0-RTT early data, safe only for idempotent requests. |
| mTLS | Both client and server verify each other’s certificates (CertificateRequest, client Certificate and CertificateVerify). Used between microservices, in zero trust and for financial and B2B APIs, with short-lived certificates from a private CA. |
Key takeaways
- Every handshake must agree on algorithms, agree on keys, authenticate the server (and in mTLS the client), and prove the transcript was not altered (Finished).
- TLS 1.2: 2 RTT. ClientHello → ServerHello, Certificate (in clear), ServerKeyExchange (signed ECDHE key), ServerHelloDone → ClientKeyExchange, ChangeCipherSpec, Finished → ChangeCipherSpec, Finished. Pre-master → master secret (PRF with both randoms) → key block.
- RSA key transport has no forward secrecy: a key stolen later decrypts recorded traffic. ECDHE fixes that; TLS 1.3 allows only ephemeral key exchange.
- TLS 1.3: 1 RTT. Key share in ClientHello; after ServerHello everything is encrypted (EncryptedExtensions, Certificate, CertificateVerify, Finished); HKDF key schedule bound to the transcript; HelloRetryRequest costs a round trip; only 5 AEAD suites; RSA key exchange, CBC, RC4, compression and renegotiation are gone.
- Resumption: session IDs and tickets in 1.2, PSK in 1.3 (with ECDHE for forward secrecy). 0-RTT sends data in the first flight but can be replayed: idempotent requests only,
425 Too Earlyfor the rest. - SNI picks the certificate and is plaintext; ECH hides it behind a shared name; ALPN picks h2/http/1.1/h3 inside the handshake. X25519MLKEM768 is the default hybrid post-quantum key exchange and makes the ClientHello about 1.8 KB.
- Termination gives layer-7 features with plaintext inside, re-encryption adds encryption inside, passthrough keeps end-to-end TLS but loses layer 7. HSTS preload removes the first plain-HTTP request.
- Common failures: expiry, clock skew, missing intermediate, missing SNI, version or cipher mismatch, ALPN mismatch, unknown CA, missing client certificate, middleboxes dropping large hellos.
- mTLS: CertificateRequest → client Certificate + CertificateVerify → server verifies, then authorises. SPIFFE IDs in short-lived certificates, rotated automatically; sidecar or ambient meshes do it for you. Combine mTLS for services with JWTs for users.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
Which four goals must every TLS handshake achieve?
Self-checkQuestion
In a TLS 1.2 ECDHE handshake, what does the ServerKeyExchange message contain?
Self-checkQuestion
Why can the TLS 1.2 server not send its Finished message in its first flight?
Self-checkQuestion
An attacker has recorded two years of a company’s TLS 1.2 traffic. Today they steal the server’s private RSA key. Which recorded sessions can they decrypt?
Self-checkQuestion
Which two reasons explain why TLS 1.3 removed RSA key transport? (Choose two.)
Choose 2
Self-checkQuestion
Read the TLS 1.2 suite TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384. Which statement is correct?
Self-checkQuestion
What exactly saves a round trip in TLS 1.3 compared with TLS 1.2?
Self-checkQuestion
A FIPS-only gateway accepts only P-256. An older client sends a key share for X25519 only, but lists P-256 in supported_groups. What happens?
Self-checkQuestion
Which two statements about the TLS 1.3 key schedule are correct? (Choose two.)
Choose 2
Self-checkQuestion
How many cipher suites does TLS 1.3 define, and what do they specify?
Self-checkQuestion
Which is not removed in TLS 1.3?
Self-checkQuestion
A security engineer opens a TLS 1.3 capture and wonders why the certificate is not visible. What is the reason?
Self-checkQuestion
On a 100 ms round-trip path, when does the response to the first request arrive on a brand-new TCP + TLS 1.2 connection, and on TCP + TLS 1.3?
Self-checkQuestion
A team moves its API from TCP + TLS 1.3 to QUIC (HTTP/3). Which round trip does that save on a new connection?
Self-checkQuestion
Why did TLS 1.2 session IDs often fail to resume behind a load balancer, while session tickets worked?
Self-checkQuestion
Why must TLS 1.2 ticket keys be rotated every few hours?
Self-checkQuestion
Which two requests are reasonable to accept as TLS 1.3 0-RTT early data? (Choose two.)
Choose 2
Self-checkQuestion
An edge server receives POST /transfer as 0-RTT early data. What is the correct behaviour?
Self-checkQuestion
An attacker replays a recorded 0-RTT first flight to a different edge server of the same CDN. Why can this succeed?
Self-checkQuestion
An old monitoring script connects to https://203.0.113.10/health on a server that hosts 50 sites, and fails with a hostname mismatch. Browsers using the site names work. What is the likely cause?
Self-checkQuestion
What does Encrypted Client Hello hide, and when does it actually help?
Self-checkQuestion
A gRPC client fails to connect with the alert no_application_protocol through a new proxy. What is the fix?
Self-checkQuestion
Why do browsers deploy a hybrid post-quantum key exchange (X25519MLKEM768) years before large quantum computers exist?
Self-checkQuestion
After a browser update enabled the hybrid post-quantum key share, users behind one corporate firewall see HTTPS sites hang, with no certificate error. What is the most likely cause?
Self-checkQuestion
A team needs path-based routing and a WAF at the load balancer, and compliance requires encryption on the internal network too. Which mode fits?
Self-checkQuestion
A partner API verifies client certificates at an ALB, which forwards the identity to backends in a header. Which two controls are required? (Choose two.)
Choose 2
Self-checkQuestion
In a TLS 1.3 mutual TLS handshake, what does the client send to prove it owns its certificate?
Self-checkQuestion
A new deployment of the orders service cannot call payments. Payments’ logs show the alert unknown_ca. Which cause fits best?
Self-checkQuestion
The reviews service presents a perfectly valid mesh certificate and calls POST /charge on payments. What should happen, and why?
Self-checkQuestion
Which two statements about SPIFFE and short-lived certificates are true? (Choose two.)
Choose 2
Self-checkQuestion
What is the main difference between a sidecar service mesh and Istio’s ambient mode?
Self-checkQuestion
An internal API currently uses a shared API key. A security review asks for caller identity that is useless if captured from logs or traffic. Which approach fits best?
Self-checkQuestion
The notes say TLS 1.3 offers 0-RTT for reconnection. Which correction is most important?