Hallo Habr, ich heiße Ilja, ich arbeite im Plattformteam der Firma Exness. Wir entwickeln und implementieren grundlegende Infrastrukturelemente, die von unseren Produktentwicklungsteams genutzt werden.
In diesem Artikel möchte ich meine Erfahrungen bei der Implementierung der Technologie encrypted SNI (ESNI) in der Infrastruktur öffentlicher Webseiten teilen.

Die Verwendung dieser Technologie wird das Sicherheitsniveau bei der Arbeit mit öffentlichen Webseiten erhöhen und den internen Sicherheitsstandards der Firma entsprechen.
Zunächst möchte ich darauf hinweisen, dass die Technologie nicht standardisiert ist und sich noch im Entwurf befindet. Allerdings unterstützen CloudFlare und Mozilla sie bereits (im ). 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 'Client Hello'-Nachricht des TLS-Handshake zu verschlüsseln. So sieht ein Client Hello mit ESNI-Unterstützung aus (statt des üblichen SNI sehen wir ESNI):

Um ESNI zu verwenden, sind drei Komponenten erforderlich:
- DNS;
- Unterstützung seitens des Clients;
- Unterstützung seitens des Servers.
DNS
Es müssen zwei DNS-Einträge hinzugefügt werden – A, und TXT (Der TXT-Eintrag enthält den öffentlichen Schlüssel, mit dem der Client SNI verschlüsseln kann.) – siehe unten. Außerdem muss die Unterstützung für DoH (DNS über HTTPS) gegeben sein, da die verfügbaren Clients (siehe unten) die ESNI-Unterstützung ohne DoH nicht aktivieren. Das ist sinnvoll, da ESNI eine Verschlüsselung des Ressourcennamens impliziert, auf den wir zugreifen, was bedeutet, dass es sinnlos ist, DNS über UDP anzusprechen. Darüber hinaus ermöglicht die Verwendung von den Schutz vor „Cache Poisoning“-Angriffen in diesem Szenario.
Derzeit stehen , darunter:
CloudFlare (Check My Browser → Encrypted SNI → Learn More), deren Server bereits jetzt ESNI unterstützen, was bedeutet, dass wir für CloudFlare-Server im DNS mindestens zwei Einträge haben – A und TXT. Im folgenden Beispiel fordern 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 gemäß der Vorlage _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 Sicht von DNS sollten wir DoH (vorzugsweise mit DNSSEC) verwenden und zwei Einträge hinzufügen.
Unterstützung durch den Client
Wenn wir über Browser sprechen, ist derzeit . Hier ist eine Anleitung, wie Sie die Unterstützung für ESNI und DoH in FireFox aktivieren können. Nachdem der Browser konfiguriert ist, sollten wir ungefähr folgendes Bild sehen:

zur Überprüfung des Browsers.
Natürlich muss TLS 1.3 zur Unterstützung von ESNI verwendet werden, da ESNI eine Erweiterung von TLS 1.3 ist.
Für Testzwecke des Backends mit ESNI-Unterstützung haben wir einen Client auf go, aber dazu später mehr.
Unterstützung durch den Server
Aktuell 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 beschlossen, ein eigenes Front-End-Komponent (ESNI-Proxy) zu erstellen, das die Terminierung von TLS 1.3 mit ESNI und das Proxying von HTTP(S)-Verkehr an ein Upstream, das ESNI nicht unterstützt, ermöglicht. Dies erlaubt den Einsatz der Technologie in bestehenden Infrastrukturen, ohne die grundlegenden Komponenten – also die aktuellen Webserver, die ESNI nicht unterstützen – zu verändern.
Zur Veranschaulichung hier ein Diagramm:

Ich möchte darauf hinweisen, dass das Proxy so konzipiert wurde, dass es die TLS-Verbindung ohne ESNI terminieren kann, um Clients ohne ESNI zu unterstützen. Auch das Kommunikationsprotokoll mit dem Upstream kann sowohl HTTP als auch HTTPS mit einer TLS-Version unter 1.3 sein (wenn das Upstream 1.3 nicht unterstützt). Dieses Schema bietet maximale Flexibilität.
Wir haben die Unterstützung für ESNI auf go von übernommen. Ich möchte gleich darauf hinweisen, dass die Implementierung selbst recht komplex ist, da sie Änderungen an der Standardbibliothek crypto/tls erfordert und daher ein 'Patching' GOROOT vor dem Build benötigt.
Für die Generierung von ESNI-Schlüsseln haben wir verwendet (auch ein Produkt von CloudFlare). Diese Schlüssel werden für die Verschlüsselung/Dekodierung von SNI verwendet.
Wir haben den Build mit Go 1.13 auf Linux (Debian, Alpine) und MacOS getestet.
Ein paar Worte zu betriebswirtschaftlichen Besonderheiten
Der ESNI-Reverse-Proxy stellt Metriken im Prometheus-Format bereit, wie zum Beispiel rps, Upstream-Latenz & Antwortcodes, fehlgeschlagene/erfolgreiche TLS-Handshakes & TLS-Handshake-Dauer. Auf den ersten Blick schien dies ausreichend zu sein, um zu bewerten, wie der Proxy mit dem Traffic umgeht.
Außerdem haben wir vor der Nutzung Lasttests durchgeführt. Die Ergebnisse sind unten aufgeführt:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Ausführen des 6-minütigen Tests @ https://esni-rev-proxy.npw:443
50 Threads und 1000 Verbindungen
Thread-Statistiken Avg Stdev Max +/− Stdev
Latenz 1.77s 1.21s 7.20s 65.43%
Anfragen/Sek. 13.78 8.84 140.00 83.70%
206357 Anfragen in 6.00m, 6.08GB gelesen
Anfragen/Sek: 573.07
Übertragung/Sek: 17.28MB Die Lasttests haben wir rein qualitativ durchgeführt, um die Szenarien mit und ohne ESNI-Reverse-Proxy zu vergleichen. Wir „haben“ Traffic lokal bereitgestellt, um „Störungen“ in den Zwischenkomponenten auszuschließen.
Mit Unterstützung von ESNI und dem Proxying zum Upstream über HTTP erhielten wir etwa ~ 550 rps von einer Instanz, wobei der durchschnittliche CPU-/RAM-Verbrauch des ESNI-Reverse-Proxys beträgt:
- 80% CPU-Auslastung (4 vCPU, 4 GB RAM-Hosts, Linux)
- 130 MB Mem RSS

Zum Vergleich, RPS für denselben Upstream von Nginx ohne TLS-Terminierung (HTTP-Protokoll) ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' --timeout 15s
Ausführen des 6-minütigen Tests @ http://lb.npw:80
50 Threads und 1000 Verbindungen
Thread-Statistiken Avg Stdev Max +/− Stdev
Latenz 1.11s 2.30s 15.00s 90.94%
Anfragen/Sek. 23.25 13.55 282.00 79.25%
393093 Anfragen in 6.00m, 11.35GB gelesen
Socket-Fehler: verbinden 0, lesen 0, schreiben 0, Zeitüberschreitung 9555
Nicht-2xx oder 3xx Antworten: 8111
Anfragen/Sek: 1091.62
Übertragung/Sek: 32.27MB Das Vorhandensein von Zeitüberschreitungen deutet darauf hin, dass es an Ressourcen mangelt (wir haben 4 vCPU, 4 GB RAM-Hosts, Linux verwendet), und tatsächlich ist das potenzielle RPS höher (wir erhielten Zahlen bis 2700 RPS auf leistungsfähigeren Ressourcen).
Abschließend möchte ich betonen, dass die ESNI-Technologie vielversprechend aussieht. Es gibt noch viele offene Fragen, wie zum Beispiel die Speicherung des öffentlichen ESNI-Schlüssels in DNS und das Rotieren von ESNI-Schlüsseln – diese Fragen werden intensiv diskutiert, und die letzte Version des Entwurfs (zum Zeitpunkt des Schreibens) von ESNI ist bereits .
Quelle: habr.com
