TLS & Security Primer
Right after the TCP handshake for curl https://api.example.com/user/42, the client and server run a second handshake — the TLS 1.3 handshake — to agree on a cipher, exchange a key, prove the server's identity, and switch the connection to encrypted records. Only then does the actual HTTP request go on the wire. Four sections build the picture: why TLS exists at all (confidentiality, integrity, authenticity); the crypto building blocks (symmetric, asymmetric, AEAD, ECDHE, HKDF); an interactive TLS 1.3 handshake walkthrough; and PKI — certs, chains, CAs, mTLS. Then a quick reference.
Why TLS — confidentiality, integrity, authenticity
Without TLS, anyone on the path between curl and the server — the coffee-shop AP, the building's router, every ISP hop, the datacenter switch — can read and modify every byte. TLS solves three problems at once, and confusing them is the most common security mistake.
When curl https://api.example.com/user/42 runs in 2026, the bytes leaving your laptop look like noise to anyone tapping the wire. Strip out HTTPS — make it plain http:// — and the same request becomes a clear-text packet capture: URL, headers, cookies, the JSON body, the response. TLS exists to prevent that, but the "encryption" framing undersells what it does.
The three guarantees
- Confidentiality — encryption hides the payload from any passive observer on the path. AES-128-GCM with a per-connection key means an attacker reading the bytes off the wire gets ciphertext that's computationally indistinguishable from random.
- Integrity — an active attacker can't flip bits in transit without detection. AEAD (Authenticated Encryption with Associated Data) produces a per-record MAC alongside the ciphertext; flip one bit and decryption rejects the entire record. This is what defeats "inject a script into the response" attacks on hostile networks.
- Authenticity — the client knows it's actually talking to
api.example.com, not a man in the middle pretending to be it. This is the cert chain's job: the server proves it holds the private key for a certificate that some root CA the client already trusts has signed (directly or via an intermediate) and that namesapi.example.com.
Drop any one of these and the others mean very little. Confidentiality without integrity lets an attacker rewrite the encrypted bytes to corrupt the plaintext (the basis of padding-oracle attacks). Confidentiality + integrity without authenticity lets a man in the middle establish a fully encrypted channel to itself, decrypt, proxy, re-encrypt — "perfect" encryption to the wrong endpoint.
Where TLS sits in the stack
TLS layers on top of TCP (or in QUIC's case, the equivalent is baked into the transport itself). After the TCP three-way handshake, the very next packets are the TLS handshake, then encrypted records carry whatever the application protocol — HTTP, IMAP, SMTP, AMQP, MQTT, gRPC — wants to send. TLS doesn't know or care what's inside; it just wraps a bidirectional byte stream in encrypted, integrity-protected records.
+-----------------------------+ | HTTP / IMAP / AMQP / gRPC | application protocol (cleartext at this level) +-----------------------------+ | TLS record layer | 5-byte header + AEAD-encrypted payload +-----------------------------+ | TCP | in-order byte stream +-----------------------------+ | IP | best-effort packets +-----------------------------+
The record layer is the bottom half of TLS: once the handshake establishes session keys, every byte the app sends is sliced into records ≤16 KB with a tiny header (type + version + length) followed by an AEAD-encrypted payload. The sequence number is implicit per connection and feeds the nonce, so the same record sent twice would have a different ciphertext and the receiver detects the replay.
"Where does TLS terminate?"
Termination = where the bytes get decrypted. In a typical production setup, api.example.com resolves to a load balancer (Cloudflare, AWS ALB, an Envoy fleet) that holds the cert, runs the TLS handshake, decrypts the request, and then speaks one of three protocols to the actual application server:
- Plain HTTP over a private network — common inside a VPC; relies on network-level isolation for security. One misrouted packet or compromised host and you've lost everything.
- Re-encrypted TLS with a less-strict cert — internal CA, longer-lived certs. The LB re-encrypts to the backend so the bytes are never in cleartext on the network.
- mTLS with service identity — both sides authenticate each other via certificates; covered in Section 04. The modern service-mesh default.
Knowing the termination point matters because it's where certs are configured, where TLS-version policy is enforced, and where most TLS-related outages live ("cert expired on the LB" is a recurring postmortem theme).
The takeaway. "TLS gives three guarantees: confidentiality (AEAD encryption hides the payload), integrity (the AEAD MAC detects any in-flight tampering), and authenticity (the cert chain proves you're talking to the named host, not a man in the middle). The record layer wraps any TCP byte stream — HTTP, IMAP, gRPC — in encrypted records ≤16 KB. TLS usually terminates at a load balancer; backend traffic is then plain HTTP, re-encrypted TLS, or mTLS depending on the trust model."
The crypto building blocks — symmetric, asymmetric, AEAD, ECDHE
Four ideas, in order: symmetric ciphers (cheap, both sides need the same key); asymmetric (expensive, no shared secret needed); AEAD (encryption + integrity in one primitive); key exchange (how you get the symmetric key without an eavesdropper seeing it). TLS 1.3 picks one of each.
Symmetric ciphers — fast bulk encryption
A symmetric cipher uses the same key to encrypt and decrypt. AES is the workhorse: 128-bit or 256-bit keys, hardware accelerated on every modern CPU (Intel AES-NI, ARM cryptographic extension), throughput easily 5+ GB/s per core. The TLS 1.3 cipher suites:
- TLS_AES_128_GCM_SHA256 — AES-128 in GCM mode, SHA-256 for HKDF. The default almost everywhere on x86 / ARM with crypto extensions.
- TLS_AES_256_GCM_SHA384 — AES-256, same shape. Required by some compliance regimes; ~30% slower than AES-128 for no real-world security improvement.
- TLS_CHACHA20_POLY1305_SHA256 — ChaCha20 stream cipher + Poly1305 authenticator. Faster than AES on CPUs without AES-NI (most older phones, some embedded). Mobile clients frequently prefer this.
That's essentially the whole list. TLS 1.2 had ~300 cipher suites; TLS 1.3 reduced it to a handful of audited combinations that all pin AEAD + ECDHE + HKDF.
Asymmetric ciphers — slow, public-key
An asymmetric scheme has a key pair: a public key anyone can know, and a private key only the owner has. Operations done with one are undone with the other. Used for signatures (sign with private, verify with public) and key agreement. They're ~1000× slower than symmetric:
- RSA-2048 — still common in cert signatures and older deployments. RSA-1024 is broken in principle (2030-ish). RSA-4096 is overkill for most.
- ECDSA P-256 — elliptic-curve signatures with ~3000-bit-RSA security at 256-bit key size. Tiny certs, fast verification, the modern default for new server certs.
- Ed25519 — Edwards-curve signatures, faster still, no parameter ambiguity, no nonce-reuse footguns. Used by SSH host keys, some root CAs are adopting it.
- X25519 — the elliptic-curve Diffie-Hellman variant used for TLS 1.3 key exchange (note: signing is Ed25519, key exchange is X25519 — different curves).
Asymmetric ops are only used at handshake — a single RSA decrypt or ECDSA sign per connection. Once the shared secret is established, every byte after is symmetric.
AEAD — encryption + integrity in one primitive
Pre-AEAD constructions did encryption and integrity separately: encrypt-then-MAC, MAC-then-encrypt, encrypt-and-MAC. All three have nasty failure modes when implemented carelessly:
# The vulnerabilities pre-AEAD enabled: BEAST (2011) — TLS 1.0 CBC-mode IV predictability → recover cookies POODLE (2014) — SSLv3 padding oracle; downgrade attack to expose data LUCKY13 (2013) — Timing side-channel in MAC-then-encrypt CBC CRIME (2012) — TLS compression + chosen-plaintext → recover headers
AEAD bundles encryption and integrity in one primitive: a single call takes (key, nonce, plaintext, associated data) and produces (ciphertext, tag). Decryption takes (key, nonce, ciphertext, tag, associated data) and either returns the plaintext or fails. There's no "decrypt then check MAC" sequence to mess up. AES-GCM and ChaCha20-Poly1305 are both AEAD. TLS 1.3 mandates AEAD for everything.
Key exchange — how the symmetric key is born
Both sides need the same symmetric key, and an eavesdropper must not be able to derive it from anything visible on the wire. Three historical approaches:
- Static RSA key exchange (TLS 1.2 and earlier, removed in 1.3): client picks a random pre-master secret, encrypts it under the server's long-lived RSA public key from the cert, ships it. Server decrypts with its RSA private key. No forward secrecy: anyone who later acquires the server's RSA private key can decrypt every TLS session ever recorded with that cert. Government adversaries actively stockpile encrypted traffic for exactly this reason — "harvest now, decrypt later."
- DHE — Diffie-Hellman Ephemeral: both sides pick a random secret per connection, exchange public values, and compute the shared secret. The server's long-term key signs the handshake but is never used as the key-exchange key — so stealing it later doesn't decrypt past traffic. Forward secrecy. The math uses big primes (2048+ bit), making this slow. Vulnerable to the Logjam attack if implemented with too-small primes.
- ECDHE — Elliptic-Curve Diffie-Hellman Ephemeral: same idea, but on an elliptic curve. ~256-bit keys give equivalent strength to 3000-bit DH at far lower cost. TLS 1.3 mandates ECDHE; X25519 is the universal default curve.
Forward secrecy is the key property: an attacker who records ciphertext today, then later obtains the server's cert private key, still cannot decrypt the recorded sessions because the ephemeral keys were never written down anywhere. Without forward secrecy, encrypted traffic is only as durable as the secrecy of the long-term cert key.
HKDF — turn one secret into many
The ECDHE shared secret is 32 bytes. TLS 1.3 needs many keys: handshake-encryption key (server-write, client-write), application traffic keys (server-write, client-write), exporter secret (for token binding, etc), resumption secret (for the next session's PSK). HKDF (HMAC-based Key Derivation Function) is the standard primitive: HKDF-Extract(salt, secret)compacts the entropy into a fixed-size PRK, then HKDF-Expand(PRK, info, len) can be called many times with different info strings to produce independent keys of any length. Output keys are computationally independent — compromising one doesn't reveal the others.
# What TLS 1.3 actually derives (abbreviated): shared_secret = ECDH(client_x25519, server_x25519) handshake_secret = HKDF-Extract(salt = early_secret, secret = shared_secret) client_hs_traffic = HKDF-Expand-Label(handshake_secret, "c hs traffic", transcript_hash) server_hs_traffic = HKDF-Expand-Label(handshake_secret, "s hs traffic", transcript_hash) # ... and continues for master_secret → application keys → resumption_secret
The takeaway. "Symmetric ciphers (AES-128-GCM, ChaCha20-Poly1305) do the bulk-data encryption — fast, both sides need the same key. Asymmetric (ECDSA, Ed25519, X25519) is slow and used only at handshake for signatures and key exchange. AEAD packs encryption + integrity into one primitive; TLS 1.3 mandates it because pre-AEAD constructions kept producing padding-oracle attacks. ECDHE gives forward secrecy: recording ciphertext + later stealing the cert key still doesn't decrypt. HKDF stretches one ECDH secret into all the distinct keys the protocol needs."
TLS 1.3 handshake — one round trip to encrypted records
TLS 1.2 needed two round trips before the first byte of HTTP could flow. TLS 1.3 needs one. The client speculates the cipher and key share in ClientHello; the server confirms and immediately encrypts the rest. The savings — one RTT per connection — was the entire motivation for the rewrite.
Right after the TCP handshake for our api.example.comconnection finishes, both sides have an in-order byte stream but no encryption. The TLS handshake is the next thing on the wire, and in TLS 1.3 it fits in exactly one round trip — the steps below walk it message by message.
ClientHello. The server picks the cipher, sends its share, derives the shared secret via ECDH, and immediately encrypts the rest of the handshake under the handshake traffic key — certificate, CertificateVerify (signs the entire transcript so a downgrade attack can't hide), and Finished. Application records flow under separate application traffic keys derived via HKDF.Why TLS 1.3 is one RTT and 1.2 was two
In TLS 1.2, the client sent ClientHello (proposing ciphers), waited for ServerHello + Certificate + KeyExchange + ServerHelloDone, then sent its own ClientKeyExchange + Finished and waited for the server's Finished — two round trips before any application data. TLS 1.3 made one critical change: the client sends its key share in the very first message, speculatively picking a cipher it expects the server to also support. If the server agrees, it derives the shared secret right away and the ServerHello reply can include everything else, already encrypted. Total handshake: 1 RTT.
TLS 1.2 (2 RTT): TLS 1.3 (1 RTT):
client -> ClientHello client -> ClientHello + key_share
server -> ServerHello server -> ServerHello + key_share +
-> Certificate {EE, Cert, CertVerify, Finished}
-> ServerKeyExchange client -> {Finished}
-> ServerHelloDone -> [Application Data]
client -> ClientKeyExchange
-> ChangeCipherSpec
-> Finished
server -> ChangeCipherSpec
-> Finished
client -> [Application Data]0-RTT — when the request rides in the very first packet
If the client has connected to this server before and kept the resumption secret, it can do better than 1 RTT: in the same packet as ClientHello, the client can include encrypted "early data" under a PSK-derived key. Total: 0 RTT. The HTTP request literally rides in the very first IP packet of the connection.
Catch: 0-RTT is replay-vulnerable. An attacker who captures the early-data packet can replay it later — TLS 1.3 has no built-in anti-replay for 0-RTT (it would require server-side state that scales badly across a CDN). So 0-RTT is only safe for idempotent requests. CDNs typically allow 0-RTT for GETs but force a 1-RTT handshake for POST/PUT/DELETE.
What TLS 1.3 dropped (and why)
The biggest design choice was negative — what to remove. TLS 1.3 deleted every algorithm that had ever produced a CVE:
- Static RSA key exchange — no forward secrecy (see Section 02). All key exchange must be (EC)DHE.
- MD5 and SHA-1 in any handshake role — both have practical collision attacks.
- RC4 — biased keystream, exploitable to recover plaintext after enough samples.
- CBC-mode block ciphers — source of BEAST, POODLE, LUCKY13. All AEAD now.
- Compression at the TLS layer — enabled CRIME. Application-layer compression (gzip) is still fine because attackers can't inject chosen plaintext into the HTTP body the way they could into a TLS record.
- Renegotiation — replaced by separate KeyUpdate. Renegotiation was the source of a 2009 vulnerability where the server confused two clients' data.
Net effect: ~300 cipher suites in TLS 1.2 became ~5 in TLS 1.3 — all of them safe by construction. No more "disable the bad ones in your config" — the bad ones simply don't exist.
Transcript integrity defeats downgrade attacks
The CertificateVerify message is a signature, with the server's long-term private key, over the SHA-256 hash of every handshake message so far — the "transcript hash." If an attacker tampered with the ClientHello to force a weaker cipher, the transcript the server signs will differ from what the client computed, and the signature won't verify. This is the defense against the entire family of downgrade attacks (Logjam, FREAK).
Also worth knowing: the Finished message is a MAC over the transcript using the handshake key, so both sides cross-check that they computed the same transcript hash. Any in-flight tampering breaks the MAC and the connection aborts before any application data is sent.
What you can see with openssl
$ openssl s_client -connect api.example.com:443 -tls1_3 -msg CONNECTED(00000005) >>> TLS 1.3, Handshake [length 00ff], ClientHello <<< TLS 1.2, Handshake [length 007a], ServerHello <<< TLS 1.3, Handshake [length 0024], EncryptedExtensions <<< TLS 1.3, Handshake [length 092e], Certificate <<< TLS 1.3, Handshake [length 0114], CertificateVerify <<< TLS 1.3, Handshake [length 0034], Finished >>> TLS 1.3, Handshake [length 0034], Finished --- Server certificate subject=CN = api.example.com issuer=C = US, O = Let's Encrypt, CN = R3 Cipher : TLS_AES_128_GCM_SHA256 Protocol : TLSv1.3 Verification: OK
The takeaway. "TLS 1.3 is 1 RTT because the client sends its key share in ClientHello speculatively; the server picks the cipher, derives the shared secret, and ships its cert + CertificateVerify + Finished all encrypted in the ServerHello reply. The client sends Finished; HTTP can ride the very next packet. 0-RTT skips the handshake using a PSK but is replay-vulnerable — idempotent requests only. CertificateVerify signs the entire transcript so downgrade attacks (Logjam, FREAK) can't hide."
PKI — certificates, chains, CAs, and mTLS
The crypto in Section 02 establishes a secure tunnel; PKI is what tells the client whether the other end of the tunnel is actually the host it asked for. The whole edifice — X.509 certs, trust roots, ACME, revocation — exists to answer that single question correctly.
X.509 — the cert format
A certificate is a binary structure (DER-encoded ASN.1, usually shown as PEM-armored Base64) that binds a public key to one or more names, signed by an issuer. The fields a TLS client actually looks at:
- Subject — the legacy "owner" name; the
Common Name(CN) field used to hold the hostname. Modern clients (Chrome since 2017) ignore CN entirely. - Subject Alternative Name (SAN) — the modern field for hostnames. A single cert can list many SANs (
api.example.com,www.example.com,*.example.comwildcards). The client matches the requested hostname against this list. - Public key — the server's public key (RSA-2048, ECDSA P-256, etc). The server signs the handshake with the matching private key to prove ownership.
- Validity period — Not Before / Not After timestamps. Public CAs (Let's Encrypt etc.) issue 90-day certs; renewal must be automated.
- Issuer — the next cert up in the chain by name.
- Extensions — Key Usage (digitalSignature, keyEncipherment), Extended Key Usage (serverAuth, clientAuth), Basic Constraints (is this cert a CA?), CRL Distribution Points, OCSP responder URL, SCT list (Certificate Transparency).
- Signature — the issuer's signature over everything above, computed with the issuer's private key.
$ openssl x509 -in cert.pem -noout -text | head -25
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 04:c5:ab:...
Signature Algorithm: ecdsa-with-SHA256
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Mar 1 00:00:00 2026 GMT
Not After : May 30 23:59:59 2026 GMT
Subject: CN = api.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
EC Public Key: (256 bit)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
DNS:api.example.com, DNS:www.example.comThe chain — why we need intermediates
A typical chain is three certificates deep:
Leaf: CN = api.example.com (issued by R3, valid 90 days)
|
| signed by R3's private key
v
Intermediate: CN = Let's Encrypt R3 (issued by ISRG Root X1, valid ~5 years)
|
| signed by ISRG Root X1's private key
v
Root: CN = ISRG Root X1 (self-signed, valid ~30 years)
^
| pre-installed in every browser / OS trust storeWhy the intermediate? Risk isolation. The root CA's private key is the most precious key in the world for that CA — kept offline, in an HSM, in a secure facility. It signs only intermediates, never leaves. If the intermediate's key is compromised, the CA revokes that intermediate (which takes effort to propagate) and issues a new one — but the root stays safe. Compromise of the root would require every browser and OS to ship a new trust store. The root is rotated every decade or two; intermediates more often.
At handshake time, the server sends the leaf cert and any intermediates it knows about. The client walks the chain, verifying each signature using the next cert's public key, until it reaches a cert whose issuer is in the local trust store.
ACME and Let's Encrypt
Before 2016, getting a cert meant filling forms on a CA's website, paying $100+/year, manually installing files. ACME (Automated Certificate Management Environment, RFC 8555) is the protocol that automated all of it. Let's Encrypt is the free CA that runs on ACME at scale. The flow:
- Your
certbot/acme.sh/ Caddy / Traefik generates a fresh key pair. - It asks Let's Encrypt to issue a cert for
api.example.com. - Let's Encrypt challenges the client to prove control of the domain: serve a specific file at
http://api.example.com/.well-known/acme-challenge/xyz(HTTP-01) or set a DNS TXT record (DNS-01). - Let's Encrypt fetches the challenge to verify, then signs and returns the cert (90 days valid).
- Your tool installs the cert and schedules a renewal job at ~60 days.
Effect on the industry: HTTPS deployment went from ~30% of page loads in 2014 to ~95% by 2026. EV (Extended Validation, the "green bar") and OV (Organization Validation) certs are largely dead because Chrome stopped showing the green bar in 2019 — users couldn't reliably distinguish DV from EV anyway, and EV gave no real-world security benefit.
Revocation — when a cert needs to be killed
A cert can be issued for 90 days but compromised on day 7. How does the client know? Three mechanisms, all imperfect:
- CRL (Certificate Revocation List) — the CA publishes a signed list of revoked serial numbers. Was the original approach; the lists got huge (MB-scale), download cost dominated. Browsers mostly stopped checking.
- OCSP (Online Certificate Status Protocol) — client makes a real-time HTTP request to the CA's OCSP responder for "is serial number X still valid?" Adds an extra RTT per cert, leaks browsing history to the CA, and if the responder is down the browser must either soft-fail (insecure) or hard-fail (deny). Mostly soft-failed in practice.
- OCSP stapling — server periodically fetches a fresh OCSP response from the CA and serves it alongside the cert during the TLS handshake. Client gets the freshness guarantee without the extra RTT or privacy leak. The modern default.
Also: Certificate Transparency (CT) — every publicly-trusted cert must be logged in append-only CT logs before issuance. Browsers refuse certs without enough SCTs (Signed Certificate Timestamps). This makes mis-issuance public: if a CA issues a cert for your domain that you didn't request, you can see it in the logs and complain. CT killed the 2015 Symantec-issued-fake-google-certs scandal cycle.
mTLS — when the client has a cert too
In standard TLS, only the server presents a cert. Mutual TLS (mTLS) requires the client to present one too — both sides authenticate each other. The cert chain validation runs in both directions.
Use cases:
- Service-to-service in a service mesh — Istio, Linkerd, Consul Connect default to mTLS for every internal RPC. The proxy sidecar handles cert issuance, rotation, and validation; the application sees a plain HTTP or gRPC call.
SPIFFE/SPIREstandardize the identity model: every workload gets a SPIFFE ID likespiffe://prod/payment-serviceembedded in its cert. - API authentication for high-security endpoints— banks, government, B2B integrations. The client's cert identifies it more strongly than any API key (it can't be exfiltrated via log files the way an API key can).
- VPN-replacement (zero-trust networks) — Cloudflare Access, Tailscale, Google BeyondCorp. Each device gets a cert; access is gated by cert validation rather than network location.
Cert pinning — when even PKI isn't enough
For high-value mobile apps (banking, messaging), the standard trust store is too broad — hundreds of CAs, any of which could issue a fraudulent cert for your domain (CT makes this public but doesn't prevent it). Pinning hardcodes the expected cert (or its public-key SPKI hash) in the app; if the cert presented during the handshake doesn't match, the connection is refused regardless of what the trust store says.
Browsers tried this with HPKP (HTTP Public Key Pinning) but it was deprecated in 2018 — too easy to brick your own site by losing the pinned key. Mobile pinning is still common because the app vendor controls the update channel and can ship a new pin. Caveat: a missed rotation can lock users out, requiring an emergency app update.
The takeaway. "A cert binds a public key to a hostname (in the SAN field), signed by an intermediate CA, signed by a root CA in the client's trust store. The leaf rotates every 90 days via ACME / Let's Encrypt; the root rotates every decade. Revocation in practice is OCSP stapling + Certificate Transparency. mTLS adds client-side cert authentication — the service mesh default for internal RPC, and the zero-trust replacement for VPNs. Pinning hardcodes the expected cert for high-value mobile apps."
Quick reference
Six questions worth being able to reason about cold, and five red flags to spot in a code review.
What three guarantees does TLS provide and how does it provide each?
Confidentiality — AEAD encryption (AES-128-GCM or ChaCha20-Poly1305) hides the payload from any passive observer. Integrity — the same AEAD primitive produces a per-record MAC; any in-flight tampering fails decryption. Authenticity — the cert chain proves the server holds the private key for a cert that names the requested host and is signed (transitively) by a root in the client's trust store. Drop any one and the others are nearly useless: no authenticity lets a MITM run a perfect encrypted channel to itself.
Why is RSA key exchange (vs ECDHE) considered legacy?
No forward secrecy. With static RSA key exchange, the client encrypts the pre-master secret with the server's long-term RSA public key. Anyone who later acquires the RSA private key can decrypt every TLS session ever recorded with that cert — this is the "harvest now, decrypt later" threat model that motivates state-actor traffic capture. ECDHE generates an ephemeral key per connection that's thrown away; stealing the long-term cert key later doesn't reveal it. TLS 1.3 removed static RSA entirely.
Walk me through the TLS 1.3 handshake — what's in each message?
ClientHello: supported ciphers, X25519 public key, SNI = hostname. ServerHello: chosen cipher, server X25519 public key (now both sides can compute the shared ECDH secret and derive handshake keys via HKDF). Then, encrypted under the handshake key: EncryptedExtensions, Certificate (the leaf + intermediates), CertificateVerify (signature over the entire handshake transcript — defeats downgrade attacks), Finished (MAC of the transcript). Client validates the chain, sends its own Finished encrypted. Both sides switch to application traffic keys. HTTP rides the next packet. Total: 1 RTT before app data.
What does it mean to "validate a cert chain" — what is the browser actually checking?
Five things in sequence: (1) Subject Alternative Name list contains the hostname being connected to. (2) Current time is within Not Before / Not After. (3) Extended Key Usage includes serverAuth. (4) For each cert in the chain, the next-up cert's public key successfully verifies its signature; the topmost cert's issuer matches a cert in the local trust store. (5) Revocation check via OCSP staple (or soft-fail OCSP), and enough SCTs from CT logs are present. Failure of any step aborts the handshake with a hard error — no "ignore and continue."
What is 0-RTT and why is it dangerous?
0-RTT (early data) lets a client re-using a previous session's PSK send encrypted application data in the very first packet, alongside ClientHello — skipping the handshake entirely. The danger: the data is replay-vulnerable. An attacker who captures a 0-RTT packet can replay it later; TLS 1.3 has no built-in anti-replay for early data (it would require server-side state that scales badly across a CDN). Safe only for idempotent requests; CDNs typically allow 0-RTT for GET, force 1-RTT for POST / PUT / DELETE.
What is mTLS and where would you use it?
Mutual TLS — the client also presents a certificate, and the server validates the client's chain just like the client validates the server's. Primary uses: service-to-service authentication in a service mesh (Istio / Linkerd / Consul Connect, often via SPIFFE identities), high-security API authentication where an API key is too weak (banks, B2B integrations), and zero-trust network access (Cloudflare Access, Tailscale) replacing VPNs. The mesh case is the most common — mTLS is the default for east-west traffic in any modern Kubernetes deployment.
Red flags in code review
- Disabling cert verification in production HTTP clients.
curl -k,requests.get(..., verify=False),InsecureSkipVerify: true. This makes TLS into mTLS-without-mTLS: encryption to some server, with no way to detect a MITM. Almost always a sign someone hit a cert error and worked around it instead of fixing it. - HTTP + app-level AES "encryption" instead of TLS. Rolling your own crypto means rolling your own padding-oracle / replay / nonce-reuse bugs. TLS is a decade of professional cryptographers' output; use it and put your application logic on top, not next to it.
- Old TLS versions (1.0 / 1.1) enabled on a public service. BEAST, POODLE, LUCKY13 all live there. Modern browsers refuse 1.0 / 1.1 by default but server-side compatibility flags often still allow them. Disable explicitly:
ssl_protocols TLSv1.2 TLSv1.3in nginx. - Self-signed cert with no internal PKI / trust distribution. Means every client either runs with verification disabled (see flag 1) or has a copy of the cert pinned in code (rotation nightmare). Stand up a tiny internal CA (smallstep, Vault PKI) and distribute the root cert via config management instead.
- 0-RTT enabled for non-idempotent endpoints. Allowing 0-RTT POSTs lets an attacker replay a previously-captured purchase, transfer, or DB-mutation request. Most CDNs let you allow-list 0-RTT per route — keep it to GET and HEAD, never mutating verbs.