11 min read · Guide · Network
How it works · Network · Security

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.

Parts01 – 07 SpecRFC 6066 · RFC 9849 PrereqTLS handshake

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.

01

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.

02

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.

03

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.

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

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

  3. 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.

And with no SNI at all?

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.

SNI · RFC 6066

"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.

ALPN · RFC 7301

"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.

The one-line answer

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.

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

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

  3. 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.

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

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

  3. 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.

The usual suspects

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.



A closing note

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.

Q1. TLS 1.3 encrypts almost the entire handshake. Why is the SNI hostname still sent in plaintext?
Q2. SNI and ALPN both travel in the ClientHello. What is the actual difference?
Q3. A legacy health-check script opens TLS to a multi-tenant load balancer without sending SNI. What does the server do?

Found this useful?