What is a TLS Handshake
Key Takeaways
- TLS 1.3 completes the handshake in 1-RTT — down from 2-RTT in TLS 1.2
- The handshake negotiates cipher suites, authenticates the server, and derives session keys via ECDHE
- Most handshake failures come from cipher suite mismatches, expired certificates, or incomplete chains
- 0-RTT resumption trades latency for replay protection — use it only for idempotent requests
A TLS handshake is the sequence of messages exchanged between a client and server before any encrypted application data flows. It accomplishes three things: agreeing on cryptographic parameters (cipher suite, key exchange algorithm), authenticating the server’s identity via its certificate, and deriving shared session keys that encrypt everything after. Every HTTPS connection starts with a handshake — it’s the gate that must open before data moves.
Why it matters
- Cipher negotiation — client and server must agree on a cipher suite (e.g., TLS_AES_256_GCM_SHA384) before encryption can begin. No agreement = no connection.
- Server authentication — the handshake is where the server proves its identity by presenting a certificate signed by a trusted CA. Without this step, you’re talking to anyone.
- Key derivation — ECDHE key exchange during the handshake produces a shared secret that both sides use to derive symmetric session keys. The private keys never cross the wire.
- Forward secrecy — TLS 1.3 mandates ephemeral key exchange. Even if the server’s long-term private key is later compromised, past sessions remain encrypted.
- Latency budget — the handshake adds round trips before data flows. TLS 1.2 costs 2-RTT; TLS 1.3 costs 1-RTT. On high-latency links, this difference is measurable.
How it works
TLS 1.3 (1-RTT):
- ClientHello — client sends supported cipher suites, key shares (ECDHE public values for one or more groups like X25519 or P-256), and supported TLS versions
- ServerHello — server selects cipher suite and key share group, sends its ECDHE public value
- Server sends certificate — the server’s TLS certificate (end entity + intermediates) for client validation
- CertificateVerify — server signs the handshake transcript with its private key, proving it owns the certificate
- Server Finished — server sends a MAC over the entire handshake transcript, confirming integrity
- Client validates — client verifies the certificate chain, checks the CertificateVerify signature, and confirms the Finished MAC
- Client Finished — client sends its own Finished message. Application data can now flow encrypted.
The critical difference from TLS 1.2: the client sends key shares in the first message (speculative), so the server can compute the shared secret immediately. No extra round trip for key exchange.
TLS 1.2 comparison: In TLS 1.2, the handshake requires a separate ServerKeyExchange message and an additional round trip before the client can send Finished. The server certificate is also sent unencrypted (visible to network observers), whereas TLS 1.3 encrypts it after the ServerHello.
In real systems
Nginx cipher configuration:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:P-256;
Debugging with curl:
curl -vvv https://example.com 2>&1 | grep -A5 "SSL connection"
# Shows negotiated cipher, TLS version, and certificate chain
Wireshark inspection — filter with tls.handshake to see each handshake message. In TLS 1.3, everything after ServerHello is encrypted, so you’ll only see ClientHello and ServerHello in cleartext. Use SSLKEYLOGFILE to decrypt the rest during debugging.
Kubernetes Ingress — when using ingress-nginx, the handshake happens at the ingress controller. Backend pods never see TLS. If the ingress controller’s cipher list doesn’t match what clients support, connections fail silently at the edge.
Where it breaks
Cipher suite mismatch — the client offers cipher suites the server doesn’t support (or vice versa). Common when legacy clients (Java 7, old Android) connect to servers configured for TLS 1.3 only. The handshake fails with handshake_failure alert. Fix: check ssl_ciphers includes at least one suite the client supports, or configure fallback suites for legacy clients.
Incomplete certificate chain — the server sends its end-entity certificate but omits the intermediate CA certificate. The client can’t build the chain to a trusted root and aborts with unknown_ca or certificate_unknown. This works in Chrome (which fetches intermediates via AIA) but fails in curl, Java HttpClient, and most programmatic clients. Always configure the full chain in ssl_certificate.
Version mismatch — server configured with ssl_protocols TLSv1.3 only, but the client only supports TLS 1.2. The server sends a protocol_version alert and drops the connection. This happens when hardening configs without checking client compatibility.
Operational insight
0-RTT resumption in TLS 1.3 lets returning clients send encrypted data in the very first message — zero round trips of latency. But 0-RTT data has no replay protection at the TLS layer. An attacker who captures the initial flight can replay it. This means 0-RTT should only carry idempotent requests (GET, not POST with side effects). Nginx supports 0-RTT via ssl_early_data on, but your application must check the Early-Data header and reject non-idempotent requests that arrive in early data. Most teams enable 0-RTT without this check, creating a subtle replay vulnerability.
Related topics
Ready to Secure Your Enterprise?
Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.