How SNI tells one IP address which site you meant
Server Name Indication is a few dozen bytes in the first packet of every HTTPS connection. Those bytes are why one server can hold a thousand certificates, why your CDN bill is what it is, and why a network observer still knows which site you visited. Worth knowing in full.
The virtual-hosting problem SNI solves
SNI (Server Name Indication) is the TLS extension that puts the hostname the client wants inside the ClientHello, the first message of the TLS handshake. It exists because of a deadlock that plain HTTP never had.
HTTP solved one-IP-many-sites back in the 1.1 era with the Host header: the server reads the header, picks the right virtual host, done. HTTPS broke that trick. The Host header is part of the HTTP request, the HTTP request is encrypted, and the encryption keys come out of a handshake in which the server must already have presented a certificate. A certificate is pinned to a name. So the server needs the name to pick the certificate, and the name is locked inside a payload it can't decrypt until the certificate is picked.
For years the workaround was crude: one IP address per HTTPS site. With IPv4 space already scarce, that put a hard ceiling on how many secure sites a host, or the internet, could carry. SNI breaks the deadlock by moving the name out of the encrypted payload and into the handshake itself, where the server can read it before any cryptography happens. It was specified in RFC 3546 back in 2003 and lives today in RFC 6066, the TLS extension definitions document.
Certs are pinned to names
A certificate for shop.example is useless for blog.example. Present the wrong one and the client's validation fails before a single byte of HTTP moves.
Host arrives too late
The HTTP Host header travels inside the encrypted channel. By the time the server could read it, the certificate decision is long past. The handshake needed its own copy of the name.
IPs don't scale to sites
Pre-SNI HTTPS meant one IPv4 address per site. A modern CDN edge packs thousands of tenants behind a handful of addresses. SNI is the only reason that arithmetic works.
What actually goes on the wire
SNI is extension number zero in the ClientHello's extension list: the server_name extension. It carries a list of names (in practice always exactly one), each tagged with a name type. Only one type was ever defined: host_name, a DNS hostname. RFC 6066 is strict about the contents — no trailing dot, and literal IP addresses are explicitly not permitted. If you connect to a raw IP, your client simply omits the extension.
# Inside the ClientHello extension list
00 00 # extension type = server_name (0)
00 11 # extension length = 17
00 0f # server_name_list length = 15
00 # name_type = host_name (0)
00 0c # hostname length = 12
73 68 6f 70 2e 65 78 # "shop.ex"
61 6d 70 6c 65 # "ample" → shop.example Two properties of this layout matter more than the bytes themselves. First, it sits in the ClientHello, which means it is sent before key exchange, before certificates, before anything. There is no shared secret yet, so the extension is plaintext by construction — even in TLS 1.3, which encrypts nearly everything else in the handshake. This is not an oversight; it is the whole point. The name has to be readable precisely because nothing can be decrypted yet.
Second, it stopped being optional in practice. RFC 8446 (TLS 1.3) lists server_name among the mandatory-to-implement extensions, and every mainstream browser, HTTP client, and TLS library sends it by default. The clients that don't are museum pieces — and Part 07 covers exactly how they fail.
How servers route on SNI
The extension was invented for certificate selection, but the infrastructure world noticed something better: the hostname is sitting in the first packet of the connection, readable without any keys. That makes SNI the cheapest routing signal in the entire stack, and three distinct patterns grew out of it.
- 01
Terminate and select
The classic move. nginx matches the SNI value against its server_name directives and hands back that block's certificate; Apache, Caddy, and every managed load balancer do the equivalent. One listener on :443, hundreds of sites, each with its own cert, selected per-handshake by the name in the ClientHello.
- 02
Route without decrypting
A layer-4 proxy can peek at the SNI field and forward the still-encrypted stream to the right backend without ever holding a private key. HAProxy's req.ssl_sni and SNI-passthrough modes in load balancers and reverse proxies work this way. End-to-end encryption stays intact; the proxy is just reading the envelope.
- 03
Multi-tenant edges
CDNs and Kubernetes ingress controllers take it to the limit: thousands of tenant hostnames behind a few anycast IPs, each handshake dispatched by SNI to the right tenant's certificate and origin pool. The Kubernetes API server itself uses SNI to demultiplex aggregated API servers behind one endpoint.
The server has nothing to route on, so it falls back to a default certificate — in nginx, whichever server block is marked default_server (or simply the first one). Some servers send a unrecognized_name alert instead. Either way, a client expecting shop.example and receiving the default tenant's cert fails validation with a name mismatch. Remember this failure shape; it accounts for most SNI tickets ever filed.
SNI vs ALPN: name selection vs protocol selection
SNI and ALPN get confused constantly because they travel together: both are ClientHello extensions, both are answered in the server's first flight, and both shape what the connection becomes. But they answer two entirely different questions.
"Which site am I?"
Carries the hostname. The server uses it to pick the certificate and, often, the backend. It decides identity: which of the many sites behind this IP the handshake is for. Sent by the client; the server never echoes it back, it just acts on it.
"Which language next?"
Carries the application protocols the client speaks — h2, http/1.1, h3. The server picks exactly one and echoes it back. It decides what runs on top once the handshake finishes, with no extra round trip to negotiate it.
SNI picks the certificate; ALPN picks the protocol. Name selection versus protocol selection, in the same ClientHello, resolved in the same round trip. A typical connection to a CDN edge uses both at once: SNI routes it to the right tenant, ALPN upgrades it to HTTP/2.
The privacy problem: SNI leaks where you're going
Everything that made SNI useful to load balancers makes it useful to anyone else on the path. As the rest of the stack got encrypted — page contents under TLS, DNS queries under DoH and DoT — the plaintext SNI field quietly became the last reliable signal of which site you are visiting. A passive observer can't read what you do on a site, but the hostname itself is right there in packet one.
This is not theoretical. National firewalls and corporate middleboxes filter on the SNI field directly: read the hostname, compare against a blocklist, reset the connection. It is cheaper and more precise than IP blocking, because one CDN IP hosts thousands of innocent bystanders and the SNI names exactly the tenant to kill.
The cat-and-mouse response was domain fronting: put an innocuous hostname in the plaintext SNI, and the real, blocked hostname in the encrypted Host header, exploiting the fact that big CDNs routed on Host after terminating TLS. Censorship circumvention tools leaned on it for years, until the major clouds closed the mismatch — partly under pressure from the same governments it evaded. The episode proved the point in both directions: whoever can read the SNI controls the connection, and hiding the name needs to be a protocol feature, not a trick.
Encrypted ClientHello: how ECH hides the name
The fix had a false start. ESNI, the first attempt, encrypted just the SNI extension and left the rest of the ClientHello alone — which turned out to be both fragile and fingerprintable, and the design was abandoned. Its successor encrypts the entire inner handshake offer: Encrypted ClientHello, now standardised as RFC 9849.
The chicken-and-egg from Part 01 applies here too: you can't encrypt the first message of a handshake with keys from that handshake. ECH escapes the loop by getting key material out of band, from DNS.
- 01
A public key, published in DNS
The provider fronting the site (the client-facing server) publishes an ECH configuration — a public key plus a public_name — in the site's HTTPS/SVCB DNS record (RFC 9460). The client fetches it during resolution, ideally over encrypted DNS so the lookup itself doesn't leak.
- 02
Two hellos, one inside the other
The client builds the real ClientHelloInner — true SNI, true ALPN, everything — encrypts it under the published key, and wraps it in a decoy ClientHelloOuter whose plaintext SNI carries only the provider's public_name. An observer sees a handshake to the provider, indistinguishable from every other ECH handshake to that provider.
- 03
The relay completes the real handshake
The client-facing server decrypts the inner hello and completes the handshake for the real site, either terminating it itself or passing it to the backend. Every site behind that provider shares one outer name, so they form an anonymity set: the bigger the provider, the less the outer handshake reveals.
Deployment status, as verified against the spec and its publication record: ECH is an IETF Proposed Standard (it spent years as draft-ietf-tls-esni before publication as RFC 9849). Firefox and Chrome ship client support, Cloudflare enables it across its edge, and clients GREASE the extension — sending decoy ECH values on ordinary connections — so that real ECH traffic doesn't stand out. The honest caveat: it only helps when the DNS lookup is also encrypted, and a passive observer still sees the destination IP. ECH shrinks the leak from "exact hostname" to "some site behind this provider", which is precisely as good as the provider is big.
Debugging SNI from the command line
Almost every SNI bug presents the same way: the wrong certificate came back. The tools below let you control the name you send independently of the address you connect to, which is the whole diagnostic game.
- 01
Send a chosen name. openssl s_client
openssl s_client -connect 203.0.113.7:443 -servername shop.example sends exactly that SNI to exactly that address. Run it twice — once with -servername, once with -noservername — and diff the certificates that come back. If they differ, you've just watched SNI selection happen; the second one is the server's default cert.
- 02
Pin the address, keep the name. curl --resolve
curl -v https://shop.example --resolve shop.example:443:203.0.113.7 connects to the IP you chose while still sending the proper SNI and Host header. This is how you test one specific origin behind a load balancer, or a new server before its DNS cutover, without breaking the handshake the way curl https://203.0.113.7 would.
- 03
Watch it on the wire. tshark
tshark -i any -Y "tls.handshake.extensions_server_name" prints every SNI crossing the box, no decryption required — a neat demonstration of the privacy problem from Part 05, and the fastest way to confirm what a misbehaving client is actually sending, rather than what its docs claim.
hostname mismatch / certificate name mismatch — the client got the default cert. Nine times out of ten it didn't send SNI: ancient Java, an old wget, an embedded HTTP library, or a health checker probing the bare IP.
works in browser, fails in script — the script connects by IP address. RFC 6066 forbids IPs in server_name, so the extension is silently dropped. Use --resolve or a hosts entry instead.
unrecognized_name alert — the server knows none of its certs match the name you sent. Check for typos and for a missing server block on the new tenant.
intermittent wrong cert behind a proxy — an SNI-passthrough tier is routing on a name the terminating tier doesn't expect. Compare the SNI at both hops with tshark.
SNI is a good example of how internet plumbing actually evolves. A pragmatic patch for a hosting problem became the routing signal an entire industry of CDNs and load balancers was built on, then turned out to be the biggest privacy hole left in TLS, and is now being sealed by ECH — which had to reach all the way into DNS to find a key that could exist before the handshake does. For the deeper TLS context around it, the TLS deep dive and the HTTPS guide pick up where this page stops.
Three quick checks before you close the tab
Test what you just read about SNI.
Pick an answer for each. The right one reveals a short explanation; the wrong ones do too. No scoring — these exist so the mental model travels with you.
Read
further.
- IETF · RFC 6066TLS Extensions: Extension DefinitionsThe current home of server_name. Section 3 is SNI itself — short, and the source for Part 02's wire format.
- IETF · RFC 9849TLS Encrypted Client HelloECH, standardised. ClientHelloInner/Outer, the client-facing server, anonymity sets, and GREASE — Part 06 in full rigor.
- IETF · RFC 7301Application-Layer Protocol NegotiationThe other half of Part 04. Worth skimming just to see how small a well-scoped extension can be.
- IETF · RFC 9460SVCB and HTTPS DNS Resource RecordsThe DNS record type that carries ECH configurations to clients — the out-of-band channel that breaks the chicken-and-egg.
- Cloudflare blogEncrypted Client Hello — the last puzzle piece to privacyThe operator's view of deploying ECH at edge scale, including why ESNI had to be abandoned first.
- IETF · RFC 8446The Transport Layer Security Protocol · Version 1.3Where the rest of the handshake got encrypted, and where server_name became mandatory to implement (section 9.2).