Upgrading a listener to TLS 1.3 changes what an observer can read, but not as much as the version number suggests. The ClientHello still travels before any encryption is negotiated, and in an ordinary connection the requested server name sits there in plaintext. Encrypted Client Hello is the mechanism that hides it, and it is a separate deployment with its own prerequisites. Run these four steps before writing a policy that assumes either behaviour.

What the handshake actually exposes

TLS 1.3 encrypts the server's certificate and most of the handshake that follows the ServerHello. Everything the client sends first is outside that protection, because the keys do not exist yet. The server name indication is part of that first flight, which is why a passive observer can still read it without breaking anything.

The ECH specification, RFC 9849, changes this by encrypting an inner ClientHello for a cooperating server. The outer handshake remains, carrying a public name the network can see, so the deployment still needs an outer identity that makes sense to intermediaries. That outer name is the part people miss when they expect the connection to become anonymous. It is a cover name, not a hidden one, and it has to resolve and authenticate like any other.

None of this hides the delivery path. Source and destination addresses, ports, packet sizes and timing remain observable, and those often say more about who is talking than the name does. Name resolution and application-level relationships are yet another layer, each with its own visibility.

Read a ClientHello instead of assuming

The version in the ServerHello does not tell you what the client sent. Look at the first flight directly, from a position that represents the observer you care about.

openssl s_client -connect app.plant.example:443 -servername app.plant.example -tls1_3 </dev/null> 2>/dev/null | head -20
sudo tcpdump -ni eth0 -c 40 'tcp port 443' -w /tmp/tls.pcap
tshark -r /tmp/tls.pcap -Y 'tls.handshake.extensions_server_name' -T fields -e tls.handshake.extensions_server_name

The first command performs a handshake and shows the negotiated parameters, including the protocol version and the name you asked for. The second captures the first packets of a real connection, and the third extracts the SNI field from that capture. If the field is populated, the observer you emulated can read it.

Capture on the client side and on the path separately when the two disagree, because a local capture may show a name that a middlebox later rewrites or drops. Also capture from a host that is not terminating TLS itself, since a terminating proxy sees plaintext by design and proves nothing about what the network sees.

Treat an ECH extension as a claim, not proof

Seeing an ECH extension in a ClientHello does not mean the connection used ECH. Clients send GREASE ECH when they have no usable configuration, precisely so that their handshakes look uniform. The extension is present, the inner hello is a decoy, and the actual name is still readable.

dig +short HTTPS app.plant.example
openssl s_client -connect app.plant.example:443 -tls1_3 -status </dev/null> 2>/dev/null | grep -i -E 'ech|status_request'

The first command shows whether the origin publishes an HTTPS resource record with an ECH configuration parameter, which is how clients learn the key. The second looks for handshake status information, though a client's success message is the stronger evidence. Acceptance is what matters: a server that does not hold the matching key cannot decrypt the inner hello, and the connection falls back.

Check the client too, since browser support and configuration differ by version and platform. A policy built on "modern browsers use ECH" is a guess about a fleet, not a fact about your fleet. Inventory the versions that actually connect, then test one of each. The same page that works from a current desktop browser may fail to hide anything from an older client on the same network.

Write the observer's scope into the policy

State where the observer sits and what is visible at that point before deciding what a control can rely on. A browser's connection to a website, an outer VPN transport and a TLS-terminating proxy each expose a different set of facts, and a rule written for one position rarely transfers cleanly to another.

If a control depends on reading SNI, test how the deployed clients behave when ECH is available and when it is not. Then record the compatibility consequence, because a filter that silently stops matching is worse than one that was never deployed.

Finally, keep the uncertainty in the record. Replacing an old universal claim with a newer one leaves the same gap under a different protocol name, and the next reviewer will believe it just as firmly.

Erthyglow kevrennek