yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: Encrypted Client Hello (ECH) in GetPageSpeed NGINX Packages
The Problem: The Last Plaintext Leak in TLS 1.3
TLS 1.3 encrypts nearly everything in the handshake except one field: the SNI (Server Name Indication). Even with a fully modern TLS 1.3 deployment, an on-path observer can read exactly which hostname a client is requesting. For multi-tenant hosting, CDNs, and shared front ends, this is a meaningful privacy gap — the observer learns which site among many on the same server the client visited.
The Fix: Encrypted Client Hello
ECH closes that gap. The relevant specification, RFC 9849, has reached Proposed Standard. ECH is now shipped in GetPageSpeed’s RPM and DEB packages, available in stock NGINX, NGINX-MOD, and EDGE. The vendor claims to be the first RPM vendor shipping a server-side ECH-capable NGINX.
How It Works
ECH sends two ClientHellos instead of one:
- The outer ClientHello names a shared public “cover” hostname — the only name an observer sees.
- The inner, encrypted ClientHello carries the real hostname, encrypted to a public key published in DNS via the
ech=parameter of an HTTPS resource record (RFC 9460).
The server decrypts the inner ClientHello and routes accordingly.
Configuration Surface
ssl_ech_file— valid in bothhttpandservercontext.$ssl_ech_status— the server’s own verdict on ECH.$ssl_ech_outer_server_name— the outer (cover) name.
nginx-ech-keygen writes keys into an http-level include, /etc/nginx/conf.d/ech-keys.conf, which stock nginx.conf already picks up. The first file listed is advertised in retry-configs; every later one is kept for decryption only.
Two Common Misconceptions
1. Your DNS zone must be DNS-only. If the HTTPS record sits behind a proxying CDN, the CDN is the TLS endpoint and publishes its own ECH keys — your ssl_ech_file is never touched. That is the CDN’s privacy feature, not yours.
2. Clients need encrypted DNS (DoH or DoT). An HTTPS record fetched over plaintext port 53 leaks the hostname to exactly the observer ECH is meant to defeat, so browsers will not use ECH without it.
What ECH Does and Does Not Hide
ECH hides which name you asked for among names sharing a server. It does not hide that you connected, nor the IP address. For multi-tenant hosting, CDNs, or shared front ends, this is a substantial gain. For a single site on a dedicated IP, it buys very little — the address alone identifies the site, and reverse lookup or certificate transparency finishes the job.
Implementation: Backport to OpenSSL 3.5 LTS
ECH landed upstream in OpenSSL 4.0, but these packages ship openssl35 — an ABI-isolated build of OpenSSL 3.5 LTS with a CVE stream tracked to April 2030. Rather than swap the fleet onto a new major release, the ECH implementation from the DEfO project was backported onto 3.5 LTS with the same API surface OpenSSL 4.0 exposes.
NGINX itself needed no patching. ssl_ech_file is upstream NGINX, compiled in automatically when the TLS library supports ECH. The same rebuild also carries EDGE over to openssl35.
Monitoring
$ssl_ech_statusgives the server’s verdict:SUCCESS— the real hostname travelled encrypted.GREASE— the client supports ECH but never received your HTTPS record. This is a DNS problem, not a TLS one.
- Can also be checked from a shell by reading the key from DNS and handing it to OpenSSL via
-ech_config_list.
Key Rotation
ECH keys are meant to be short-lived. Two constraints shape rotation:
- NGINX reads keys at config parse time, so a new key does nothing until reload.
- A client that cached your HTTPS record encrypts to the old key, so dropping it immediately breaks them. The repeated
ssl_ech_filedirective handles this half.
A packaged timer, nginx-ech-rotate.timer, rotates daily, keeps three generations loaded, shreds anything older, and regenerates the include. Keys live in /etc/nginx/ech as 0640 root:nginx inside a 0750 directory, generated under umask 077 and renamed into place. Nothing runs until a public name is set; the timer is not enabled on install.
DNS Record Republishing
The timer rotates the key, but the ECHConfigList a client encrypts to comes from the HTTPS DNS record — left alone, the record goes stale on first rotation. Since nginx-mod-ech 1.30.4-61 (and edge-ech 1.30.4-6), the package closes that loop: nginx-ech-publish runs as the second ExecStart of the rotation unit and republishes the new value.
- The Cloudflare provider reuses certbot’s credentials, updates in place, and treats a failed lookup as a hard stop so it cannot create duplicate records.
- Providers are drop-in executables under
/usr/libexec/nginx-ech-publish/. - It ships inert.
Certificate Caveat
The cover name’s certificate is presented in the outer handshake, in the clear. Do not put the names you are hiding in its SAN list — one certificate covering both cover and secret name hands back exactly what ECH concealed, and it will pass every test because the handshake works fine. Issue them separately.
Failure Mode
If NGINX cannot decrypt, it falls back to the outer ClientHello, serves the public name’s server block, and hands back a retry-config with the current key. The client recovers by itself — a wasted round trip, not an outage. This is also the strongest argument for getting the cover name’s certificate right, since every stale client lands on it.
Public Demo
ech-test.getpagespeed.com runs the exact stack on public 443 with daily key rotation, and reports what your browser actually negotiated. A JSON endpoint at /status.json is available. Verified against Chrome/Chromium, Firefox 147, and openssl s_client -ech_config_list. The Firefox lab issue turned out to be DNS every time, not interop — a hosts entry means the ECHConfig is never fetched at all.
Limitations
- Clients with no ECH support are unaffected.
- Split mode is not supported — NGINX implements shared mode only.
- ECH requires TLS 1.3. Keep TLS 1.2 enabled so non-ECH clients still get a page rather than a handshake failure.
- New keys need a reload, not a signal-free hot swap.
Availability
ECH is in the main production repository in stock NGINX, NGINX-MOD, and EDGE, with rotation tooling in nginx-ech, nginx-mod-ech, and edge-ech.
- RPM: A plain
dnf upgrade nginx(ornginx-mod) from the Extras repository gets the OpenSSL 3.5-linked build. - DEB: Debian and Ubuntu got the same builds in late August 2026 across Ubuntu 20.04/22.04/24.04 LTS and Debian 12/13 on amd64 and arm64.
apt-get install nginx-echis the DEB equivalent, with config paths following Debian convention (/etc/default/rather than/etc/sysconfig/).
Operational Notes for the OpenSSL 3.5 Switch
- If using a PKCS#11 HSM, install
openssl35-pkcs11(replacesquictls-pkcs11) and mirror engine/provider config into/etc/pki/openssl35/openssl.cnf. - FIPS posture is unchanged — these builds run the default OpenSSL provider even on
fips=1hosts, as the QuicTLS-linked builds did before.
Read the full article: Encrypted Client Hello in NGINX: Now Shipping in Our RPMs and DEBs
