Course outline

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:

GoalWhy it mattersTLS 1.2 does it withTLS 1.3 does it with
Agree on a version and algorithmsBoth sides must speak the same cipherClientHello list → ServerHello choiceSame, plus supported_versions
Agree on keys nobody else knowsAll data is encrypted with themServerKeyExchange + ClientKeyExchange (ECDHE)key_share in both hellos
Authenticate the server (and optionally the client)Otherwise you might be talking to an attackerCertificate + signature in ServerKeyExchangeCertificate + CertificateVerify
Prove nobody tampered with the handshakeAn attacker could delete strong options from the hellosFinished (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).

Client192.0.2.10
Recorderon the path
shop.example.com198.51.100.20:443
ClientHello
Step 1 of 11
1

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. 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. 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. 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. 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. 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_SHA256Means
ECDHEKey exchange: ephemeral elliptic-curve Diffie-Hellman (forward secrecy)
RSAAuthentication: the certificate holds an RSA key, which signs the ECDHE parameters
AES_128_GCMBulk cipher: AES with a 128-bit key in GCM, an AEAD mode (encrypts and authenticates together)
SHA256Hash 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

Client192.0.2.10
shop.example.com198.51.100.20:443
ClientHello + key_share
Step 1 of 9
1

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. 1. ClientHello + key_share (client → server)

    supported_versions lists 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. 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. 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. 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:

SuiteNotes
TLS_AES_128_GCM_SHA256Mandatory; the most used
TLS_AES_256_GCM_SHA384Longer key; often required by government profiles
TLS_CHACHA20_POLY1305_SHA256Fast in software; preferred on phones without AES hardware
TLS_AES_128_CCM_SHA256For constrained IoT devices
TLS_AES_128_CCM_8_SHA256Short 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

RemovedWhy
RSA key transport and static Diffie-HellmanNo forward secrecy; padding-oracle attacks
CBC-mode ciphers with a separate MACPadding-oracle and timing attacks (Lucky 13 and others)
RC4, 3DES, export ciphers, MD5 and SHA-1 signaturesBroken or too weak
CompressionLeaked secrets through response sizes (the CRIME attack)
RenegotiationA source of attacks and complexity; replaced by KeyUpdate and post-handshake authentication
Custom Diffie-Hellman groupsServers 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 mechanismsUnified 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.

Client ATCP + TLS 1.2
Server A
Client BTCP + TLS 1.3
Server B
SYNSYN

Race clock (100 ms round trip)

LaneRound trips usedClock
A: TLS 1.200 → 50 ms
B: TLS 1.300 → 50 ms
Step 1 of 8
1

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:

SetupRound trips before the requestResponse at (100 ms RTT)At 200 ms RTT
TCP + TLS 1.23400 ms800 ms
TCP + TLS 1.32300 ms600 ms
TCP + TLS 1.3, HelloRetryRequest3400 ms800 ms
TCP + TLS 1.3, PSK resumption2 (no certificate, cheaper)300 ms600 ms
TCP + TLS 1.3, 0-RTT1 (TCP only)200 ms400 ms
QUIC (TLS 1.3 inside)1200 ms400 ms
QUIC with 0-RTT0100 ms200 ms
Reused connection0100 ms200 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

Client192.0.2.10
Attackerrecords, replays
Edge serversbank.example.com
ClientHello + pre_shared_key + key_share
Step 1 of 3
1

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:

  1. 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.
  2. 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

Client192.0.2.10
Wi-Fi observersees plaintext only
Shared server203.0.113.10:443
ClientHello: SNI = shop.example.com
Step 1 of 4
1

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.

Backends (private subnet)
Client192.0.2.10
Load balancershop.example.com
Backend 110.0.1.11
Backend 210.0.1.12
🔒 TLS 1.3

What the load balancer sees

SNI

shop.example.com

Method + path

POST /api/cart

Headers, cookies

all readable

Step 1 of 3
1

The client’s TLS handshake ends at the load balancer, which holds the certificate and private key for shop.example.com.

ModeWhere the certificate livesWhat the load balancer can doInside trafficTypical use
Termination (offload)Load balancerEverything layer 7: route by path and host, add headers, WAF, caching, compressionPlain HTTPMost web apps behind an AWS ALB, nginx or a CDN
Re-encryption (bridging)Public cert at the load balancer, internal certs on backendsEverything layer 7Encrypted with a second TLS sessionCompliance (card or health data), zero trust
PassthroughBackends onlyRoute by SNI or port only; no paths, headers, WAF or cacheEncrypted end to endEnd-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.

Mobile appor curl, Java
Middleboxold firewall
shop.example.com198.51.100.20:443
ClientHello
Step 1 of 3
1

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 causeFix
certificate_expired (45), NET::ERR_CERT_DATE_INVALIDRenewal failed, or the client’s clock is wrongAutomate renewal and alert well before expiry; sync time with NTP
“not yet valid” on one device typeDevice 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 fineServer sends only the leaf, not the intermediateDeploy the full chain; test with openssl s_client
Hostname mismatch only from some clientsSNI missing, so the default certificate is servedMake 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 highUpgrade the client; keep the server at TLS 1.2 minimum
handshake_failure (40), “no shared cipher”No common cipher suite or groupFix 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 proxyEnable 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 alertA middlebox drops the large post-quantum ClientHello, or MTU problemsUpdate 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

orders (client)spiffe://example.com/ns/shop/sa/orders
payments (server)payments.shop.svc
ClientHello + key_share
Step 1 of 9
1

An ordinary TLS 1.3 ClientHello. Nothing in it says “I have a certificate”: the server decides whether to ask.

#How it works

  1. 1. The server sends its certificate, as in normal TLS

    The handshake starts exactly like a normal one.

  2. 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. 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. 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, or bad_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).

Mesh CASPIRE or istiod
orders app
Proxy A
Proxy B
payments app
SVID …/sa/ordersSVID …/sa/payments
Step 1 of 4
1

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 keyJWT (bearer token)mTLS
What is sentA secret string in a headerA signed token with claims and an expiryOnly a proof of holding a private key
If intercepted or loggedUsable until rotatedUsable until it expiresUseless without the private key
IdentifiesAn application or accountA user or service, with claims (roles, scopes)A workload or device (certificate subject, SPIFFE ID)
LayerApplication (HTTP)Application (HTTP)Transport (TLS)
Survives a TLS-terminating proxyYesYesNo: the proxy must forward the identity
OperationsEasy to issue; hard to rotate everywhereNeeds a signing key and an issuerNeeds a CA, issuance, rotation, trust bundles
Best forSimple public APIs, meteringEnd-user identity across servicesService-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:

TopicSummary
TLS 1.2 vs 1.3TLS 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.
mTLSBoth 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 Early for 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.