Zum Inhalt

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 gesetztem network.trr.mode, aber leerem network.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. curl und openssl s_client auf 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 seine ECHConfig ü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 mit ech= 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