Hallo Habr, ich bin Ilja und arbeite im Plattformteam von Exness. Wir entwickeln und implementieren grundlegende Infrastrukturkomponenten, die von unseren Produktentwicklungsteams genutzt werden.
In diesem Artikel möchte ich meine Erfahrungen mit der Implementierung der Technologie encrypted SNI (ESNI) in der Infrastruktur öffentlicher Websites teilen.

Die Nutzung dieser Technologie wird das Sicherheitsniveau bei der Arbeit mit öffentlichen Websites erhöhen und den internen Sicherheitsstandards der Firma entsprechen.
Zunächst möchte ich darauf hinweisen, dass die Technologie noch nicht standardisiert ist und sich weiterhin im Entwurf befindet. CloudFlare und Mozilla unterstützen sie jedoch bereits (in ). Das hat uns zu diesem Experiment motiviert.
Ein wenig Theorie
ESNI ist eine Erweiterung des TLS 1.3-Protokolls, die es ermöglicht, SNI in der Nachricht "Client Hello" beim TLS-Handshake zu verschlüsseln. So sieht ein Client Hello mit ESNI-Unterstützung aus (anstatt des gewohnten SNI sehen wir ESNI):

Um ESNI zu nutzen, sind drei Komponenten erforderlich:
- DNS;
- Unterstützung durch den Client;
- Unterstützung durch den Server.
DNS
Zwei DNS-Einträge müssen hinzugefügt werden – A, und TXT (TXT-Eintrag enthält den öffentlichen Schlüssel, mit dem der Client SNI verschlüsseln kann) – siehe unten. Außerdem sollte Unterstützung vorhanden sein DoH (DNS über HTTPS), da verfügbare Clients (siehe unten) die Unterstützung von ESNI ohne DoH nicht aktivieren. Das ist logisch, da ESNI die Verschlüsselung des Ressourcennamens impliziert, auf den wir zugreifen, also macht es keinen Sinn, DNS über UDP anzusprechen. Darüber hinaus schützt die Nutzung vor „Cache Poisoning“-Angriffen in diesem Szenario.
Derzeit sind verfügbar , darunter:
CloudFlare (Check My Browser → Verschlüsseltes SNI → Weitere Informationen), was bedeutet, dass ihre Server bereits ESNI unterstützen, das heißt, für CloudFlare-Server im DNS haben wir mindestens zwei Einträge – A und TXT. Im folgenden Beispiel fragen wir Google DNS (über HTTPS) an:
A Eintrag:
curl 'https://dns.google.com/resolve?name=www.cloudflare.com&type=A'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "www.cloudflare.com.",
"type": 1
}
],
"Answer": [
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.210.9"
},
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.209.9"
}
]
}
TXT Eintrag, die Anfrage wird nach dem Muster gebildet _esni.FQDN:
curl 'https://dns.google.com/resolve?name=_esni.www.cloudflare.com&type=TXT'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16
}
],
"Answer": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16,
"TTL": 1799,
"data": ""/wEUgUKlACQAHQAg9SiAYQ9aUseUZr47HYHvF5jkt3aZ5802eAMJPhRz1QgAAhMBAQQAAAAAXtUmAAAAAABe3Q8AAAA=""
}
],
"Comment": "Antwort von 2400:cb00:2049:1::a29f:209."
}Aus DNS-Perspektive sollten wir DoH (vorzugsweise mit DNSSEC) verwenden und zwei Einträge hinzufügen.
Unterstützung vom Client
Wenn wir von Browsern sprechen, dann . Hier ist eine Anleitung, wie Sie die Unterstützung von ESNI und DoH in Firefox aktivieren können. Nach der Konfiguration des Browsers sollten wir etwa folgendes Bild sehen:

zur Überprüfung des Browsers.
Natürlich muss für die Unterstützung von ESNI TLS 1.3 verwendet werden, da ESNI eine Erweiterung zu TLS 1.3 ist.
Für Testzwecke mit einem Backend, das ESNI unterstützt, haben wir einen Client implementiert go, aber dazu später mehr.
Unterstützung vom Server
Derzeit wird ESNI von Webservern wie nginx/apache usw. nicht unterstützt, da sie mit TLS über OpenSSL/BoringSSL arbeiten, in denen ESNI offiziell nicht unterstützt wird.
Deshalb haben wir uns entschieden, unsere eigene Front-End-Komponente (ESNI-Reverse-Proxy) zu entwickeln, die die TLS 1.3-Terminierung mit ESNI unterstützt und HTTP(S)-Traffic an ein Upstream weiterleitet, das ESNI nicht unterstützt. Dies ermöglicht die Anwendung der Technologie in bereits bestehenden Infrastrukturen, ohne die Hauptkomponenten zu ändern – das heißt, aktuelle Web-Server, die ESNI nicht unterstützen, können verwendet werden.
Zur Veranschaulichung präsentieren wir ein Schema:

Ich möchte anmerken, dass der Proxy mit der Möglichkeit entworfen wurde, TLS-Verbindungen ohne ESNI zu terminieren, um Clients ohne ESNI zu unterstützen. Zudem kann das Kommunikationsprotokoll mit dem Upstream sowohl HTTP als auch HTTPS mit einer TLS-Version unter 1.3 sein (wenn der Upstream 1.3 nicht unterstützt). Dieses Schema bietet maximale Flexibilität.
Die Implementierung der ESNI-Unterstützung auf go haben wir von entlehnt. Ich möchte gleich anmerken, dass die Implementierung selbst recht komplex ist, da sie Änderungen an der Standardbibliothek erfordert crypto/tls und daher ein "Patching" GOROOT vor der Erstellung benötigt.
Zur Generierung von ESNI-Schlüsseln haben wir verwendet (ebenfalls ein Produkt von CloudFlare). Diese Schlüssel werden zur Verschlüsselung/Entschlüsselung des SNI verwendet.
Wir haben den Build mit Go 1.13 auf Linux (Debian, Alpine) und MacOS getestet.
Einige Worte zu den Betriebseigenschaften
Der ESNI-Reverse-Proxy bietet Metriken im Prometheus-Format, wie z.B. rps, upstream latency & response codes, fehlgeschlagene/erfolgreiche TLS-Handshakes & TLS-Handschlagdauer. Auf den ersten Blick schien dies ausreichend zu sein, um zu beurteilen, wie der Proxy mit dem Traffic umgeht.
Außerdem haben wir vor der Nutzung Lasttests durchgeführt. Die Ergebnisse finden Sie unten:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
6-minütiger Test @ https://esni-rev-proxy.npw:443
50 Threads und 1000 Verbindungen
Thread-Stats Avg Stdev Max +/- Stdev
Latenz 1.77s 1.21s 7.20s 65.43%
Req/Sec 13.78 8.84 140.00 83.70%
206357 Anfragen in 6.00 min, 6.08GB gelesen
Anfragen/Sekunde: 573.07
Übertragung/Sekunde: 17.28MB Wir haben die Lasttests rein qualitativ durchgeführt, um die Unterschiede zwischen den Szenarien mit und ohne ESNI-Reverse-Proxy zu vergleichen. Wir haben den Traffic lokal erzeugt, um «Störungen» in den Zwischenkomponenten auszuschließen.
Mit aktivierter ESNI-Unterstützung und HTTP-Proxying im Upstream haben wir etwa ~ 550 rps von einer Instanz erreicht, während der durchschnittliche CPU/RAM-Verbrauch des ESNI-Reverse-Proxys:
- 80 % CPU-Auslastung (4 vCPU, 4 GB RAM Hosts, Linux)
- 130 MB Mem RSS

Zum Vergleich: RPS für denselben Upstream nginx ohne TLS-Terminierung (HTTP-Protokoll) ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' –-timeout 15s
Führe 6 Minuten Test @ http://lb.npw:80 aus
50 Threads und 1000 Verbindungen
Thread-Statistiken Durchschnitt Std. Abw. Max +/- Std. Abw.
Latenz 1.11s 2.30s 15.00s 90.94%
Anfragen/Sekunde 23.25 13.55 282.00 79.25%
393093 Anfragen in 6.00m, 11.35GB gelesen
Socket-Fehler: Verbindung 0, Lesen 0, Schreiben 0, Timeout 9555
Nicht-2xx oder 3xx Antworten: 8111
Anfragen/Sek: 1091.62
Übertragung/Sek: 32.27MB Das Vorhandensein von Timeouts deutet darauf hin, dass Ressourcen fehlen (wir haben Hosts mit 4 vCPU und 4 GB RAM verwendet, Linux), und tatsächlich liegt das potenzielle RPS höher (wir hatten Zahlen bis zu 2700 RPS auf leistungsfähigeren Ressourcen).
Zusammenfassend möchte ich festhalten, dass die ESNI-Technologie vielversprechend aussieht. Es gibt noch viele offene Fragen, wie die Speicherung des öffentlichen ESNI-Schlüssels im DNS und die Rotation der ESNI-Schlüssel – diese Fragen werden aktiv diskutiert, und der letzte Entwurf (zum Zeitpunkt des Schreibens) des ESNI ist bereits .
Quelle: habr.com
