Encrypted Client Hello (ECH)
Encrypted Client Hello (RFC 9849) schließt das letzte große Klartext-Leck in einem TLS-Handshake: die Server Name Indication. Ohne sie kündigt jede TLS-1.3-Verbindung den Hostnamen, den Sie besuchen, im Klartext an, wo jeder Beobachter im Pfad ihn lesen kann. ECH verpackt das echte ClientHello — Hostname und alles — in ein äußeres, das nur einen gemeinsamen, öffentlichen „Cover"-Hostnamen nennt.
ssl_ech_file und die Variablen $ssl_ech_status / $ssl_ech_outer_server_name
sind im Standardpaket nginx, in nginx-mod und edge verfügbar — alle
GetPageSpeed-Builds verlinken gegen openssl35 (OpenSSL 3.5 LTS mit dem
ECH-Backport). Kein Patchen von NGINX ist erforderlich; die Direktiven stammen
von NGINX selbst und werden aktiviert, wenn die TLS-Bibliothek ECH unterstützt.
Probieren Sie es aus, bevor Sie es bauen
ech-test.getpagespeed.com betreibt
genau diesen Stack auf einem öffentlichen Port 443, mit täglich rotierten
Schlüsseln durch dasselbe unten dokumentierte Paket nginx-mod-ech. Es
meldet, was Ihr eigener Browser ausgehandelt hat, sodass Sie ein
clientseitiges DNS-Problem von einem serverseitigen unterscheiden können,
bevor Sie Ihre eigene Konfiguration anfassen.
Bringt ECH Ihnen tatsächlich etwas?
Seien Sie ehrlich zu sich selbst, was das Bedrohungsmodell angeht, bevor Sie es einsetzen.
ECH verbirgt, welchen Namen Sie unter den Namen angefordert haben, die einen Server teilen. Es verbirgt nicht, dass Sie sich verbunden haben, und es verbirgt nicht die IP-Adresse.
- Multi-Tenant-Hosting, CDNs, gemeinsame Frontends — echter, erheblicher Gewinn. Ein Beobachter erfährt, dass Sie einen Server erreicht haben, der Tausende von Websites hostet, und nichts weiter.
- Eine Website auf einer dedizierten IP — sehr geringer Gewinn. Die Adresse allein identifiziert die Website, und Reverse-DNS oder eine Certificate-Transparency-Abfrage erledigt den Rest. ECH entfernt trotzdem den SNI-String, was gegen grobe Stichwort-Zensur etwas wert ist, aber verkaufen Sie es nicht über.
Die Privatsphäre von ECH kommt von der Größe der Anonymitätsmenge hinter dem öffentlichen Namen. Ein Cover-Name, der von nur einer Website verwendet wird, schützt nichts.
Konfiguration
server {
listen 443 ssl;
listen 443 quic;
http2 on;
http3 on;
server_name secret.example.com;
ssl_certificate /etc/pki/tls/certs/secret.example.com.crt;
ssl_certificate_key /etc/pki/tls/private/secret.example.com.key;
ssl_protocols TLSv1.3;
ssl_ech_file /etc/nginx/ech/ech-20260825T000000Z.pem;
}
ECH ist inert, bis ssl_ech_file vorhanden ist. Das Hinzufügen des Pakets
ändert also nichts an einer bestehenden Bereitstellung.
ssl_ech_file ist sowohl im http- als auch im server-Kontext gültig. Wenn
Sie es auf http-Ebene setzen, wird es auf jeden TLS-Server angewendet, der es
erbt, was normalerweise das ist, was Sie wollen — und es ist harmlos für
reine-HTTP-Server, weil NGINX ECH-Schlüssel nur für Server liest, die
Zertifikate haben.
Mehrere Schlüssel und warum die Reihenfolge wichtig ist
ssl_ech_file kann wiederholt werden. Die Reihenfolge ist nicht kosmetisch:
ssl_ech_file /etc/nginx/ech/ech-20260825T000000Z.pem; # beworben
ssl_ech_file /etc/nginx/ech/ech-20260824T000000Z.pem; # nur entschlüsseln
ssl_ech_file /etc/nginx/ech/ech-20260823T000000Z.pem; # nur entschlüsseln
NGINX bewirbt nur die erste Datei in ECH-Retry-Configs. Jede spätere Datei
wird nur zur Entschlüsselung geladen. Das macht die Schlüsselrotation sicher:
Ein Client, der eine ältere ECHConfigList aus einem gecachten HTTPS-Record
aufgegriffen hat, verschlüsselt an einen älteren Schlüssel, und dieser Schlüssel
muss noch im Speicher sein, sonst schlägt der Handshake fehl.
Schlüssel werden gelesen, wenn NGINX seine Konfiguration parst. Ein neu
generierter Schlüssel bewirkt also nichts, bis nginx -s reload ausgeführt
wird.
Der öffentliche Name braucht ein Zertifikat
Der in die ECHConfig eingebackene public_name ist der Cover-Hostname, den
Clients in die äußere Klartext-SNI setzen. Wählen Sie einen, den Sie
kontrollieren, zeigen Sie ihn auf denselben Server und stellen Sie sicher, dass
Ihr Zertifikat ihn abdeckt. Wenn der ECH-Versuch eines Clients fehlschlägt —
veralteter Schlüssel, verstümmelter Record, Middlebox — fällt er auf einen
gewöhnlichen Handshake gegen diesen Namen zurück, und ein Zertifikatsfehler
dort ist ein harter Fehler, den Ihre Benutzer sehen werden.
Beobachten
log_format ech '$remote_addr "$host" ech=$ssl_ech_status '
'outer=$ssl_ech_outer_server_name';
access_log /var/log/nginx/access.log ech;
$ssl_ech_status meldet das Ergebnis des ECH-Versuchs für die Verbindung.
$ssl_ech_outer_server_name liefert den Cover-Namen, den der Client verwendet
hat, was nützlich ist, um zu bestätigen, dass der Client wirklich Ihre
ECHConfig und nicht GREASE verwendet.
Veröffentlichen des HTTPS-DNS-Records
ECH ist wertlos, bis Clients Ihre ECHConfigList finden können. Sie reist im
ech=-Parameter eines HTTPS-Ressourceneintrags:
secret.example.com. 300 IN HTTPS 1 . alpn="h3,h2" ech="AD7+DQA65wAg..."
nginx-ech-keygen --print-dns gibt den exakten ech="..."-Wert für den
aktuell beworbenen Schlüssel aus.
Zwei Anforderungen, die Leute falsch machen
Die Zone muss DNS-only sein. Wenn der Record hinter einer proxierenden CDN
liegt — ein Orange-Cloud-Cloudflare-Record zum Beispiel — beendet dieser
Anbieter TLS und veröffentlicht seinen eigenen HTTPS-Record mit seinen
eigenen ECH-Schlüsseln. Clients bekommen das ECH des Anbieters, nicht Ihres;
Ihre ssl_ech_file wird nie verwendet, weil der Anbieter der TLS-Endpunkt ist.
Das ist nicht unbedingt schlecht (die Anonymitätsmenge des Anbieters ist
enorm), aber es ist deren, nicht Ihres. Um Ihr eigenes ECH auszuliefern, muss
der Record Grey-Cloud / DNS-only sein.
Clients brauchen verschlüsseltes DNS. Ein HTTPS-Record, der über Klartext-Port 53 abgerufen wird, leakt den Hostnamen genau an den Beobachter, den ECH besiegen soll. Browser verwenden ECH daher nur, wenn der Record über DoH oder DoT angekommen ist. Benutzer ohne verschlüsseltes DNS bekommen kein ECH — nichts, was Sie auf dem Server konfigurieren, ändert das.
Cloudflare-Beispiel
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<EOF
{
"type": "HTTPS",
"name": "secret.example.com",
"ttl": 300,
"proxied": false,
"data": {
"priority": 1,
"target": ".",
"value": "alpn=\"h3,h2\" $(nginx-ech-keygen --print-dns)"
}
}
EOF
Halten Sie die TTL kurz und deutlich unter Ihrem Schlüssel-Aufbewahrungsfenster,
damit ein rotierter Schlüssel nie der einzige ist, auf den ein gecachter Record
zeigt. Sie müssen den Record nur einmal von Hand erstellen — danach hält
nginx-ech-publish ihn bei jeder Rotation aktuell (siehe unten).
Automatische Schlüsselrotation
ECH-Schlüssel sind für kurze Lebensdauer gedacht. Die Rotationswerkzeuge werden
mit jedem Build ausgeliefert: nginx-ech für das Standardpaket nginx,
nginx-mod-ech für NGINX-MOD:
dnf install nginx-ech # Standard-nginx
dnf install nginx-mod-ech # NGINX-MOD
Es bietet nginx-ech-keygen, einen nginx-ech-rotate-systemd-Timer und
/etc/sysconfig/nginx-ech-rotate. Nichts läuft, bis Sie es konfigurieren.
# 1. Cover-Hostnamen setzen.
vi /etc/sysconfig/nginx-ech-rotate # ECH_PUBLIC_NAME="ech.example.com"
# 2. Ersten Schlüssel erstellen. Schreibt /etc/nginx/conf.d/ech-keys.conf und lädt neu.
nginx-ech-keygen --init
# 3. Veröffentlichen, was es in Ihrem HTTPS-Record ausgibt.
nginx-ech-keygen --print-dns
# 4. Rotation DNS selbst neu veröffentlichen lassen (Cloudflare zuerst; siehe
# „DNS mit der Rotation in Schritt halten" unten).
vi /etc/sysconfig/nginx-ech-publish # ECH_PUBLISH_PROVIDER="cloudflare"
# 5. Rotation dem Timer übergeben (täglich standardmäßig).
systemctl enable --now nginx-ech-rotate.timer
| Befehl | Wirkung |
|---|---|
--init |
Erster Schlüssel, generiert die Include-Datei, lädt NGINX neu |
--rotate |
Neuer Schlüssel, entfernt alles über ECH_RETAIN hinaus, lädt neu |
--print-dns |
Der ech="..."-Wert für den beworbenen Schlüssel |
--list |
Aktuelle Schlüssel, markiert als beworben / nur-entschlüsseln |
Auf EDGE
Dieselben Werkzeuge werden als edge-ech ausgeliefert, umbenannt auf EDGEs
eigene Pfade: edge-ech-keygen, der edge-ech-rotate-Timer,
/etc/sysconfig/edge-ech-rotate, und Schlüssel unter /etc/edge/ech.
Jeder Schritt oben gilt mit ersetzten Namen.
ECH_RETAIN (Standard 3) steuert, wie viele Generationen geladen bleiben. Bei
täglicher Rotation ist das ungefähr ein Drei-Tage-Gnadenfenster für Clients mit
einem gecachten HTTPS-Record. Setzen Sie es über die TTL Ihres Records, nicht
darunter.
DNS mit der Rotation in Schritt halten
Rotation allein lässt DNS zurück: Die ECHConfigList, an die ein Client
verschlüsselt, kommt aus dem HTTPS-Record, nicht vom Server, also wird der
Record beim ersten Timer-Feuer veraltet. Nichts bricht — Clients mit dem
veralteten Wert bekommen ECH: failed+retry-configs, verbinden sich trotzdem
über den Cover-Namen und heilen sich selbst über die Retry-Config. Aber kein
erster Handshake bekommt je ECH, was leise den Großteil des Nutzens
wegwirft.
nginx-ech-publish schließt diese Schleife (ausgeliefert in nginx-mod-ech
seit 1.30.4-61 und edge-ech seit 1.30.4-6; noch nicht im Standardpaket
nginx-ech). Es läuft als zweites ExecStart des Oneshot-
nginx-ech-rotate.service, feuert also nur nach einer erfolgreichen Rotation:
Es liest den beworbenen Wert aus --print-dns (nur die öffentliche
ECHConfigList, nie den privaten Schlüssel) und veröffentlicht ihn neu im
HTTPS-Record. Es wird inert ausgeliefert — ohne konfigurierten Anbieter
beendet es sich, ohne etwas zu tun. Eine Sysconfig-Bearbeitung schaltet es ein:
# /etc/sysconfig/nginx-ech-publish
ECH_PUBLISH_PROVIDER="cloudflare"
ECH_ZONE_ID="..." # Zonen-ID von der Domain-Übersichtsseite
ECH_RECORD_NAME="www.example.com" # der innere Name, den Clients besuchen, NICHT der Cover-Name
| Variable | Bedeutung |
|---|---|
ECH_PUBLISH_PROVIDER |
Anbieter-Drop-in zum Ausführen; leer bedeutet nichts tun |
ECH_ZONE_ID |
Anbieter-Zonenkennung |
ECH_RECORD_NAME |
FQDN des HTTPS-Records — der ECH-fähige innere Name |
ECH_ALPN |
Der alpn=-SvcParam, der neben ech= beworben wird (Standard h3,h2) |
ECH_TTL |
Record-TTL in Sekunden (Standard 300) |
ECH_PUBLISH_CREDENTIALS |
Anmeldedaten-Datei (Standard /root/.cloudflare.ini) |
Der Cloudflare-Anbieter liest certbots dns-cloudflare-ini-Format
(dns_cloudflare_email / dns_cloudflare_api_key), sodass eine bestehende
certbot-Anmeldedatendatei wiederverwendet statt kopiert wird. Er aktualisiert
den Record an Ort und Stelle, und eine fehlgeschlagene Record-Suche ist ein
harter Stopp, nie als „kein Record" behandelt — so kann er keine doppelten
HTTPS-Records erzeugen.
Anbieter sind Drop-in-Executables unter /usr/libexec/nginx-ech-publish/.
Der Dispatcher exportiert ECH_B64, ECH_ZONE_ID, ECH_RECORD_NAME,
ECH_ALPN, ECH_TTL und ECH_PUBLISH_CREDENTIALS in die Umgebung des
Anbieters, sodass die Unterstützung von Route 53, deSEC oder jeder anderen
DNS-API ein Skript ist, das in dieses Verzeichnis gelegt wird — keine
Dispatcher-Änderungen.
Für einen Anbieter, den Sie selbst außerhalb dieses Mechanismus skripten,
funktioniert der altmodische Hook weiterhin: Fügen Sie Ihr eigenes
ExecStart-Drop-in zu nginx-ech-rotate.service hinzu, lesen Sie
nginx-ech-keygen --print-dns, und PUT-en Sie den Wert in den HTTPS-Record.
In beiden Fällen behält die Rotation ECH_RETAIN-Generationen, sodass die
Lücke zwischen dem Neuladen und der DNS-Verbreitung konstruktionsbedingt
abgedeckt ist — der vorherige Schlüssel ist noch zur Entschlüsselung geladen.
Zertifikate: Setzen Sie den inneren Namen nicht auf den Cover-Namen
Das Zertifikat des Cover-Namens wird im äußeren Handshake präsentiert, wo der Beobachter, den ECH besiegen soll, es lesen kann. Wenn eine SAN-Liste sowohl den Cover-Namen als auch die Namen abdeckt, die Sie verbergen, gibt dieses Zertifikat genau das zurück, was ECH gerade verborgen hat.
Stellen Sie sie getrennt aus: ein Zertifikat für den öffentlichen Namen, eines pro innerem Namen.
Was ein zu kurzes Fenster tatsächlich kostet
Kein Ausfall, wie sich herausstellt. Wir haben alle drei Fälle gegen diese Pakete gemessen:
Gecachte ECHConfigList des Clients |
Ergebnis |
|---|---|
| Der beworbene Schlüssel | ECH erfolgreich, Anfrage an den inneren Namen geroutet |
| Ein behaltener Nur-Entschlüsseln-Schlüssel | ECH erfolgreich, Anfrage an den inneren Namen geroutet |
Aus ECH_RETAIN herausgealtert |
ECH: failed+retry-configs, Verbindung wird trotzdem abgeschlossen |
Im dritten Fall kann NGINX nicht entschlüsseln, also fällt es auf das äußere ClientHello zurück, bedient den Serverblock des öffentlichen Namens und gibt eine Retry-Config mit dem aktuellen Schlüssel zurück. Der Client nimmt das auf und erholt sich selbst. Die Kosten sind eine verschwendete Roundtrip-Zeit, keine kaputte Seite.
Das ist auch der Grund, warum das Zertifikat des öffentlichen Namens so wichtig ist: Jeder veraltete Client landet darauf. Wenn Sie dieses Zertifikat falsch machen, verwandelt eine Rotation einen behebbaren Retry in einen Zertifikatsfehler.
/etc/nginx/conf.d/ech-keys.conf wird bei jeder Rotation generiert. Bearbeiten
Sie es nicht; es wird neu geschrieben, um dem zu entsprechen, was auf der
Festplatte liegt. Schlüssel liegen in /etc/nginx/ech, Modus 0640 root:nginx,
in einem 0750-Verzeichnis — der NGINX-Master parst die Konfiguration als
root, sodass Worker sie nie lesen müssen.
Häufiger als täglich zu rotieren ist vernünftig und das, was die ECH-Designer
vorschlagen, aber denken Sie daran, dass jede Rotation ein nginx -s reload
kostet. Überschreiben Sie mit einem Drop-in, statt die ausgelieferte Unit zu
bearbeiten:
systemctl edit nginx-ech-rotate.timer # [Timer] / OnCalendar=hourly
Client-Unterstützung
Verifiziert gegen diese Pakete, Ende-zu-Ende, über DoH gegen einen echten
ech=-HTTPS-Record, mit Erreichen von $ssl_ech_status-Erfolg:
| Client | Ergebnis |
|---|---|
| Chrome / Chromium | ECH ausgehandelt |
| Firefox 147 | ECH ausgehandelt |
openssl35 s_client -ech_config_list |
ECH ausgehandelt |
Clients, die ECH nicht unterstützen, sind nicht betroffen: Sie senden ein normales ClientHello und NGINX bedient sie normal.
Sie können jeden Client gegen unseren öffentlichen Demo-Host prüfen, ech-test.getpagespeed.com — er meldet, was Ihr Browser tatsächlich ausgehandelt hat, und liefert dieselben Daten als JSON unter /status.json.
Wenn ein Browser, der ECH unterstützt, es nicht verwendet
Fast immer DNS, nicht TLS. Insbesondere Firefox lehnt die Verwendung von ECH in mehreren Situationen ab, in die ein Testaufbau leicht gerät, und jede sieht wie ein Interop-Fehler aus, ohne einer zu sein. Unserer meldete eine Weile GREASE, genau aus diesem Grund.
- Der HTTPS-Record wurde nicht über DoH abgerufen. Firefox verwendet nur
eine
ECHConfig, die von einem vertrauenswürdigen rekursiven Resolver (TRR) kam. „DNS over HTTPS" zu aktivieren, reicht allein nicht — ein Anbieter muss ausgewählt sein. Mit gesetztemnetwork.trr.mode, aber leeremnetwork.trr.uri, löst Firefox nativ auf und sendet GREASE. - Ein System-Proxy ist im Weg. Firefox respektiert standardmäßig die
OS-Proxy-Konfiguration, einschließlich einer PAC-Datei. Wenn DoH durch diesen
Proxy geroutet wird und der Proxy es nicht transportieren will, schlägt TRR
still fehl und jede Suche fällt auf natives DNS zurück — oder hängt.
curlundopenssl s_clientauf derselben Maschine sind nicht betroffen, was das sehr überzeugend als „Serverfehler" erscheinen lässt. - Ein
hosts-Eintrag deckt den Namen ab. Ein so aufgelöster Name bekommt seineECHConfigüberhaupt nie abgerufen. - Die Adresse ist RFC 1918. TRR-Antworten mit privaten Adressen werden
standardmäßig verworfen (
network.trr.allow-rfc1918) als Anti-DNS-Rebinding-Maßnahme — was den HTTPS-Record mitech=gleich mit verwirft. - Auf macOS muss DoH aktiviert sein, damit ECH überhaupt verwendet wird.
Testen Sie gegen einen öffentlich auflösbaren Namen, der auf eine öffentliche
Adresse zeigt, mit explizit ausgewähltem DoH-Anbieter, ohne Proxy und ohne
hosts-Eintrag. about:networking#dns zeigt, ob die Antwort von TRR kam.
Einschränkungen
- Split-Modus wird nicht unterstützt. NGINX implementiert den Shared-Mode-Server, bei dem der ECH-terminierende Server auch den inneren Namen bedient. Split-Modus, bei dem ein Frontend ECH entschlüsselt und an ein separates Backend weiterleitet, ist nicht im Upstream-NGINX.
- ECH braucht TLS 1.3. Es gibt kein ECH für TLS-1.2-Verbindungen.
- Ein Reload ist erforderlich, damit neue Schlüssel wirksam werden.
Siehe auch
- HTTP/3 (QUIC) — ECH passt natürlich zu HTTP/3
- NGINX-MOD — der erweiterte Build, in dem diese Direktiven ebenfalls enthalten sind
- RFC 9849 — TLS Encrypted Client Hello
- RFC 9460 — HTTPS and SVCB resource records