This is part of a series of blog posts I'm writing about cryptography
You're at a coffee shop, visiting your bank's website. The padlock icon is in the address bar, TLS (Transport Layer Security) is protecting the connection, and nobody on the network can read your password or see your account balance.
But your ISP and the coffee shop WiFi operator can still see which website you're visiting. Your browser has to tell the server its name before encryption kicks in, like writing the recipient's address on the outside of a sealed envelope. The letter inside is private, but the address on the front is visible to everyone who handles it.
Encrypted Client Hello (ECH) hides that address. It's an extension to TLS, finalized as RFC 9849, that encrypts the website name so that network observers can't read it directly from the TLS handshake. We'll see why the name leaks, why it's hard to fix, and work through how ECH gets around each obstacle.
Your destination is showing
When your browser connects securely to a website, it starts by sending a message called a ClientHello. Think of it as a greeting: "Hi, I'd like to talk to bank.example.com, and here are the encryption methods I support." The server picks an encryption method, and the two sides set up a secure channel.
The problem is that this greeting is sent before encryption is set up. It has to be, because the whole point of the greeting is to negotiate the encryption. And right there in the greeting, in plain text, is the name of the website: the Server Name Indication, or SNI.
Step through the connection below and watch what the network observer sees.
The observer doesn't need to crack any encryption. The website name is sitting right there in the open, readable by anyone on the network path: your ISP, your employer, the coffee shop.
The SNI isn't the only thing that leaks. The ClientHello also contains an ALPN (Application-Layer Protocol Negotiation) list, which tells the server which application protocol the client wants (HTTP/2, HTTP/1.1, etc.). ECH encrypts all of this, not just the website name.
Even if you use encrypted DNS (like DNS over HTTPS) to hide your DNS lookups, the SNI still gives you away because it travels as part of the TLS connection itself.
Why does the browser say the name out loud?
If sending the website name is a privacy problem, why does the browser send it at all? Many websites share the same server. A single machine at IP address 203.0.113.42 might host a bank and a health portal. When your browser connects to that IP, the server needs to know which website you want so it can present the right identity card (called a certificate).
Without the website name, the server has no idea which certificate to show. Click a domain below and watch the server use the name to pick the right one.
You can't just delete the SNI field because the server needs it. The question is whether the browser can send it in a way that only the server can read.
You can't encrypt what you don't have a key for
The obvious fix is "encrypt the website name." But that runs into a chicken-and-egg problem. To encrypt something, you need the recipient's public key. To look up the recipient's public key, you need to know who the recipient is. And on a server hosting hundreds of websites, knowing who the recipient is requires the very website name you're trying to encrypt.
Picture an apartment building with one front door. You want to send a sealed letter to apartment 7B, but the doorman only knows to deliver it if you write "7B" on the outside, where everyone in the lobby can read it.
ECH gives the building itself a key, separate from any individual apartment. You encrypt the apartment number with the building's key and hand it to the doorman. He decrypts it, reads "7B," and delivers the letter. Everyone in the lobby saw you walk into the building, but nobody except the doorman knows which apartment you're visiting.
In networking terms, the "building" is a client-facing server, usually a CDN (content delivery network) like Cloudflare. Your browser sends two ClientHellos layered together: an outer one that says "I want to talk to the CDN" (visible to observers) and an inner one that says "I want bank.example.com" (encrypted, so only the CDN can read it).
Toggle between the two deployment modes and step through the message flow.
In shared mode, the CDN and the website run on the same machine. The server decrypts the outer layer to find the real destination, then completes the connection using the inner one.
In split mode, the CDN and the website are separate machines. The CDN decrypts the outer layer, finds the real destination, and forwards the connection to the right backend server. The CDN never sees the content of the connection because the backend handles encryption directly with your browser.
Two hellos
How does the browser build this layered message? It needs an ECH configuration (ECHConfig for short), which the website publishes in its DNS records. The ECHConfig contains the CDN's public key (used to encrypt the inner hello) and the CDN's public name (like "cdn.example.com"), which goes in the outer hello as a cover story.
The browser builds the real ClientHello first, with the actual website name and its encryption preferences. Then it builds a second, outer ClientHello with the CDN's public name as the destination, encrypts the real hello using the CDN's public key, and tucks the ciphertext (the encrypted blob) into the outer hello as an extension field.
Step through the construction.
An observer watching the network sees a ClientHello going to cdn.example.com with an opaque blob in one of its extension fields. Without the CDN's private key, there's no way to figure out what's inside.
The encryption uses HPKE (Hybrid Public Key Encryption, defined in RFC 9180). The browser generates a one-time encryption key, encrypts the inner ClientHello, and includes the one-time key alongside the ciphertext so the server can decrypt it.
To prevent tampering, the outer ClientHello is included as "associated data" during encryption. This binds the two hellos together so an attacker can't swap the outer hello for a different one while keeping the same encrypted inner hello. If any part of the outer hello changes, decryption fails.
The inner hello is also padded so that its length doesn't reveal the length of the real website name. The padding rounds the message up to a multiple of 32 bytes, so "a.com" and "longdomainname.example.org" produce blobs of similar sizes.
What if decryption fails?
When the server receives a ClientHello with an encrypted inner hello, it tries to decrypt it. If it succeeds, it uses the real website name and proceeds normally. But decryption can fail if the browser cached an old ECH configuration with a retired key, or if the server doesn't support ECH at all.
When that happens, the server falls back to the outer ClientHello (the one with the CDN's name) and completes the handshake using that. It also sends the browser a fresh ECH configuration, saying "your key was outdated, here's a new one." The browser drops the connection, opens a fresh one, and tries again.
Toggle between the success and failure paths to see the difference.
ECH configurations change over time as websites rotate encryption keys, and browsers cache them for a while. The retry flow makes sure a stale cache doesn't permanently lock a browser out; it just costs one extra connection attempt.
The fresh configuration is delivered inside the handshake's encrypted portion, so it's authenticated by the server's certificate. An attacker can't inject a fake retry configuration because they don't have the server's private key.
The browser also checks that the server's certificate matches the CDN's public name before trusting the new configuration. If the certificate doesn't match, the browser discards everything and reports an error.
Note that the initial ECH configuration, delivered via DNS, is only as trustworthy as the DNS path. An attacker who can tamper with DNS responses can strip or replace the ECHConfig. DNSSEC and encrypted DNS transports (DoH, DoT) help protect this initial delivery.
If the browser already retried once and the server still rejects ECH, something is probably misconfigured on the server side. The browser stops retrying to avoid looping.
Blending in with the crowd
If only some browsers send the encrypted inner hello, then the presence of the ECH extension itself tells an observer that this browser is trying to hide something. The observer doesn't need to decrypt anything; they just check whether the extension is there.
The fix is that ECH-capable browsers always send the extension, even when they don't have an ECH configuration for the target site. When there's no configuration, the browser fills the extension with random bytes of the same length and structure as a real encrypted inner hello. An observer sees the same extension in every ClientHello from these browsers, whether it contains real encrypted data or noise.
This technique is called GREASE (Generate Random Extensions And Sustain Extensibility). Switch between a real ECH client and a GREASE client and try to tell the difference.
You can't. The extension looks identical either way. Only the server can tell, because only the server has the private key to attempt decryption. If decryption produces a valid inner hello, it's real. If it produces garbage, the server ignores it.
Safety in numbers
ECH hides which website you're visiting, but how much privacy you get depends on how many websites hide behind the same CDN. If only one website uses a particular CDN's ECH configuration, observing a connection to that CDN immediately reveals the destination.
If 500 websites share the same CDN and ECH configuration, an observer knows the user is visiting one of those 500 but can't narrow it down. This group of indistinguishable destinations is the anonymity set.
Drag the slider to see how the anonymity set size changes the observer's odds.
Cloudflare hosts millions of websites behind the same infrastructure. When all of them use the same ECH configuration, an observer seeing a connection to Cloudflare's edge learns nothing about the real destination.
ECH doesn't help if the observer can figure out the destination through other channels. Unencrypted DNS lookups reveal the domain, and IP address lookups can narrow things down if a website has a dedicated IP. Traffic analysis (packet sizes and timing patterns) can sometimes fingerprint specific sites.
For strong privacy, ECH should be combined with encrypted DNS (DNS over HTTPS or DNS over TLS. TODO: I will write about these at some point), and the website should share its IP address with other sites in the anonymity set.
The ECH configuration is delivered through DNS HTTPS records. Your browser fetches these during normal DNS resolution and caches them. When the cache expires, the browser fetches fresh configurations, which is how servers rotate keys without breaking anything.
Where we ended up
To summarize, your browser used to announce which website you were visiting in plain text. ECH wraps the real destination inside an encrypted blob and hides it behind the CDN's name. GREASE makes ECH connections look identical to non-ECH connections on the wire. The retry mechanism handles stale keys. And the privacy scales with the number of websites behind the same CDN.