Skip to main content
How the TLS Handshake Works (Explained)

How the TLS Handshake Works (Explained)

KU
• 8 min read•Updated •37 views

A developer-focused walkthrough of the TLS 1.3 handshake, certificate trust, forward secrecy, and how to debug the errors you actually hit.

Key takeaways

  • In TLS 1.3 the client sends a ClientHello with a key share, the server answers with its own key share, certificate and Finished, and both derive the same session keys in one round trip.
  • TLS 1.2 needed two.
  • When a handshake fails, openssl s_client prints the exact verify error.

A TLS 1.3 handshake takes one round trip: the client sends a ClientHello carrying its supported ciphers, SNI hostname and an ephemeral key share, the server replies with its key share, certificate and Finished message, and both sides derive the same symmetric keys. TLS 1.2 took two round trips. Debug failures with openssl s_client, which names the exact problem.

Verified against

TLS 1.3 per RFC 9846 (which obsoletes RFC 8446), Encrypted Client Hello per RFC 9849, OpenSSL 3.5 LTS and 4.1, against the rfc-editor.org texts and the docs.openssl.org s_client man page. Checked October 2026.

What TLS actually gives you

TLS (the modern name for what people still call SSL) adds three guarantees to a TCP connection: encryption (nobody on the path can read the traffic), integrity (a flipped byte is detected and the connection torn down), and authentication (you are talking to the real server, which is the certificate part and the part that fails most often).

The trick is combining two kinds of cryptography. Asymmetric math runs briefly to agree on a shared secret over an open wire, then both sides switch to fast symmetric ciphers like AES-GCM or ChaCha20-Poly1305 for the data. The handshake is the bridge between the two.

The TLS 1.3 handshake, step by step

1. ClientHello. The client lists its cipher suites, the TLS versions it speaks, and, crucially, a key share. Instead of waiting to be told which key-exchange group to use, it guesses one and sends its ephemeral public key immediately. OpenSSL 3.5 changed its default key shares to offer the post-quantum hybrid X25519MLKEM768 alongside X25519. The ClientHello also carries Server Name Indication (SNI), the hostname it wants, so a server hosting many sites knows which certificate to present.

2. ServerHello and the encrypted flight. The server picks a cipher suite and a group and replies with its own key share. Both sides can now compute the same shared secret with ephemeral Diffie-Hellman. Everything after ServerHello is encrypted: EncryptedExtensions, the Certificate, a CertificateVerify signature proving the server holds the private key, and Finished.

3. Client verification and Finished. The client validates the certificate chain, checks CertificateVerify and the server's Finished, sends its own Finished, and application data flows.

If the client guessed a group the server does not support, the server sends a HelloRetryRequest and the client retries, costing an extra round trip.

How TLS 1.2 differed

TLS 1.2 needed two round trips. The ClientHello carried only a cipher list; the server answered with its choice and certificate; only then did the client send key material, followed by another exchange of Finished messages. TLS 1.3 collapses this by sending the key share speculatively and by cutting the cipher list to a few AEAD suites. RSA key transport, static Diffie-Hellman and CBC-mode suites are gone.

Certificates and the chain of trust

The server presents a leaf certificate for its hostname. The leaf is signed by an intermediate, which is signed by a root certificate authority. Your OS or browser ships a list of trusted roots.

The client walks the chain: each certificate must be signed by the next, none may be expired, the chain must end at a trusted root, and the requested hostname must appear in the leaf's Subject Alternative Name list. The classic misconfiguration is a server that sends its leaf without the intermediate. Browsers often paper over it with cached intermediates; curl, Java, Go and Python clients use only what the server sends plus their local roots, so the same site works in Chrome and fails in CI with unable to verify the first certificate.

Forward secrecy, SNI, and resumption

Forward secrecy: because the key shares are ephemeral, an attacker who records traffic today and steals the server's private key next year still cannot decrypt those sessions. the TLS 1.3 spec states that all its public-key key exchange modes provide forward secrecy.

SNI and ECH: the hostname travels in the ClientHello, so one IP can host thousands of sites. Classic SNI is sent in the clear, which is the gap Encrypted Client Hello (RFC 9849) closes by encrypting the inner ClientHello with a key the server publishes in DNS.

Resumption and 0-RTT: after a handshake the server can issue a ticket. Next time the client presents it and skips most of the negotiation, and it can even send early data in its first flight. The RFC is blunt about the cost: "There are no guarantees of non-replay between connections," and 0-RTT data is not forward secret. Send only idempotent requests that way, never a payment.

Inspecting a handshake

The fastest way to see what a server presents is openssl s_client. -servername sends SNI, which multi-site hosts need, and -brief prints a short summary:

code
$ openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=example.com
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768

Exact lines vary by OpenSSL version and server. To see the chain the server actually sent (not the one your client built), and to make verification failures abort with an error:

code
$ openssl s_client -connect example.com:443 -servername example.com \
    -showcerts -verify_return_error -verify_hostname example.com </dev/null

Force a version to test what a server accepts, and check the leaf's dates and names without parsing the whole dump:

code
$ openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

curl -v https://example.com gives a quicker read in one line: look for SSL connection using TLSv1.3 and the subject and issuer.

The error strings, and what each one means

These are OpenSSL's verify messages, as printed in Verify return code: or a verify error: line. Other TLS stacks wrap the same causes in different words.

Error textCauseFix
certificate has expired (code 10)Leaf or intermediate past its notAfter date, or the client clock is wrongRenew and automate renewal; check date on the client
unable to verify the first certificate (code 21)Server sent the leaf without its intermediateServe the full chain (fullchain.pem, not cert.pem)
unable to get local issuer certificate (code 20)Client has no root for this chain, often a stale CA bundle in a containerInstall ca-certificates, or add the internal root
self-signed certificate (code 18) / self-signed certificate in certificate chain (code 19)Internal CA or a TLS-inspecting proxyTrust that root explicitly; never disable verification in prod
hostname mismatch (code 62)Requested name not in the leaf's SAN listReissue with the name, or connect with the right hostname
alert handshake failure / alert protocol versionNo common protocol version, cipher or groupCompare -tls1_2 and -tls1_3 results; upgrade the old side

A wrong certificate with no other symptoms is often missing SNI: without -servername, many hosts return their default certificate. For TLS terminating at a Kubernetes ingress, fixing ingress 502 and 504 errors covers the backend half of the same failures.

Where this breaks

This walkthrough describes plain server-authenticated TLS over TCP. Mutual TLS adds a client certificate and its own failure modes; see mTLS for service-to-service auth. QUIC and HTTP/3 run the TLS 1.3 handshake inside QUIC packets, so openssl s_client over TCP will not show you that connection. TLS-inspecting corporate proxies re-sign every certificate, which makes the chain you see differ from the one the server sent. And post-quantum key shares make the ClientHello larger, which has exposed middleboxes that drop big first packets.

Questions people ask

How many round trips does a TLS 1.3 handshake take?

One for a full handshake, assuming the server accepts the key share the client guessed. The client sends ClientHello with a key share, the server responds with its key share, certificate and Finished in the same flight, and application data follows. If the server needs a different group, a HelloRetryRequest adds a second round trip. Resumed sessions with 0-RTT send data in the first flight, at the cost of replay protection.

What is the difference between SSL and TLS?

SSL is the old protocol family from Netscape; every SSL version is deprecated and insecure. TLS replaced it, and the current version is TLS 1.3, now specified in RFC 9846. People still say "SSL certificate" and "SSL error" out of habit, but any modern connection is TLS. Servers should accept TLS 1.3, fall back to TLS 1.2 if old clients need it, and refuse everything older.

The call we'd make

Serve TLS 1.3 with TLS 1.2 as a fallback, disable everything older, and let the library choose ciphers and groups; current OpenSSL defaults already prefer hybrid post-quantum key exchange. Automate certificate renewal, always serve the full chain, and reserve 0-RTT for idempotent requests. When a handshake fails, run openssl s_client before you guess, because the verify code usually names the exact problem. For how this sits above TCP and DNS, see our networking fundamentals guide, and once TLS is solid, harden what rides on top with proper HTTP security headers.

Sources

Explore topics:SecurityInfrastructure
React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). How the TLS Handshake Works (Explained). DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/tls-handshake-explained

Markdown

[How the TLS Handshake Works (Explained)](https://www.devopsness.com/blog/tls-handshake-explained) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)

KU

Kiril Urbonas

AI Engineer

585 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.