Bună, Habr! Mă numesc Ilia și lucrez în echipa de platformă a companiei Exness. Dezvoltăm și implementăm componente de infrastructură de bază utilizate de echipele noastre de dezvoltare de produse.
În acest articol, aș dori să împărtășesc experiența implementării tehnologiei encrypted SNI (ESNI) în infrastructura site-urilor publice.

Folosirea acestei tehnologii va permite creșterea nivelului de securitate în gestionarea site-urilor publice și va respecta standardele interne de securitate adoptate de companie.
În primul rând, vreau să subliniez că tehnologia nu este standardizată și încă se află în draft, însă CloudFlare și Mozilla o susțin deja (în ). Aceasta a fost o motivație pentru noi să încercăm un astfel de experiment.
Puțină teorie
ESNI este o extensie a protocolului TLS 1.3 care permite criptarea SNI în mesajul „Client Hello” al handshake-ului TLS. Iată cum arată Client Hello cu suport pentru ESNI (în locul SNI obișnuit, vedem ESNI):

Pentru a folosi ESNI, sunt necesare trei componente:
- DNS;
- Suport din partea clientului;
- Suport din partea serverului.
DNS
Trebuie adăugate două înregistrări DNS – A, și TXT (înregistrarea TXT conține cheia publică, cu ajutorul căreia clientul poate cripta SNI) – vezi mai jos. În plus, trebuie să existe suport pentru DoH (DNS over HTTPS), deoarece clienții disponibili (vezi mai jos) nu activează suportul pentru ESNI fără DoH. Este logic, deoarece ESNI implică criptarea numelui resursei la care ne adresăm, ceea ce face inutilă interogarea DNS prin UDP. Mai mult, utilizarea permite protecția împotriva atacurilor de tip „cache poisoning” în acest scenariu.
În prezent, sunt disponibili , printre care:
CloudFlare (Check My Browser → Encrypted SNI → Learn More) că serverele lor suportă deja ESNI, adică pentru serverele CloudFlare în DNS avem cel puțin două înregistrări – A și TXT. În exemplul de mai jos, cerem Google DNS (pe HTTPS):
Un înregistrare:
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 înregistrare, cererea este formată conform șablonului _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": "Response from 2400:cb00:2049:1::a29f:209."
}Așadar, din punct de vedere DNS, ar trebui să folosim DoH (preferabil cu DNSSEC) și să adăugăm două înregistrări.
Sprijin din partea clientului
Dacă vorbim despre browsere, în acest moment . iată instrucțiunile despre cum să activăm suportul ESNI și DoH în FireFox. După ce browserul este configurat, ar trebui să vedem o imagine similară:

pentru a verifica browserul.
Desigur, pentru a sprijini ESNI, trebuie folosit TLS 1.3, deoarece ESNI este o extensie a TLS 1.3.
Pentru scopuri de testare a backend-ului cu suport ESNI, am realizat un client pe go, dar despre asta puțin mai târziu.
Sprijin din partea serverului
În acest moment, ESNI nu este suportat de servere web precum nginx/apache etc., deoarece acestea funcționează cu TLS prin OpenSSL/BoringSSL, în care ESNI nu este oficial suportat.
De aceea, am decis să creăm un component front-end (proxy invers ESNI) care să sprijine terminarea TLS 1.3 cu ESNI și proxying-ul traficului HTTP(S) către upstream, care nu suportă ESNI. Aceasta permite aplicarea tehnologiei în infrastructura deja existentă, fără a schimba componentele principale – adică utilizarea serverelor web actuale care nu suportă ESNI.
Pentru ilustrarea aceasta, iată un schema:

Vă voi spune că proxy-ul a fost conceput cu posibilitatea de a termina conexiunea TLS fără ESNI, pentru a sprijini clienții fără ESNI. De asemenea, protocolul de comunicare cu upstream-ul poate fi atât HTTP, cât și HTTPS cu versiunea TLS mai mică de 1.3 (dacă upstream-ul nu suportă 1.3). Această schemă oferă flexibilitate maximă.
Implementarea suportului ESNI pe go am împrumutat-o de la . Menționez că implementarea însăși este destul de non-trivială, deoarece presupune modificări în biblioteca standard crypto/tls și, prin urmare, necesită «patching» GOROOT înainte de compilare.
Pentru generarea cheilor ESNI am folosit (de asemenea, o creație CloudFlare). Aceste chei sunt folosite pentru criptarea/decriptarea SNI.
Am testat compilarea folosind go 1.13 pe Linux (Debian, Alpine) și MacOS.
Câteva cuvinte despre caracteristicile operaționale
Proxy-ul invers ESNI furnizează metrici în format Prometheus, cum ar fi rps, latența upstream și codurile de răspuns, precum și numărul de handshake-uri TLS reușite / eșuate și durata handshake-ului TLS. La prima vedere, acestea păreau suficiente pentru a evalua cum se descurcă proxy-ul cu traficul.
De asemenea, înainte de utilizare, am efectuat teste de încărcare. Rezultatele sunt mai jos:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Rulând testul de 6m @ https://esni-rev-proxy.npw:443
50 de fire și 1000 de conexiuni
Statistici Fir Medie Deviație Max +/− Deviație
Latență 1.77s 1.21s 7.20s 65.43%
Cereri/sec 13.78 8.84 140.00 83.70%
206357 cereri în 6.00m, 6.08GB citite
Cereri/sec: 573.07
Transfer/sec: 17.28MB Am efectuat teste de încărcare pur cantitative, pentru a compara schemele utilizând proxy-ul invers ESNI cu cele fără. Am „injectat” trafic local pentru a exclude „perturbațiile” din componentele intermediare.
Așadar, cu suport pentru ESNI și proxy-ing către upstream cu HTTP, am obținut aproximativ ~ 550 rps dintr-un singur instanț, iar consumul mediu de CPU/RAM al proxy-ului invers ESNI a fost:
- 80% utilizare CPU (4 vCPU, 4 GB RAM gazde, Linux)
- 130 MB Mem RSS

Pentru comparație, RPS pentru același upstream nginx fără terminarea TLS (protocol HTTP) ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' –-timeout 15s
Rulând testul de 6m @ http://lb.npw:80
50 de fire și 1000 de conexiuni
Statistici Fir Medie Deviație Max +/− Deviație
Latență 1.11s 2.30s 15.00s 90.94%
Cereri/sec 23.25 13.55 282.00 79.25%
393093 cereri în 6.00m, 11.35GB citite
Erori socket: conectare 0, citire 0, scriere 0, timeout 9555
Răspunsuri non-2xx sau 3xx: 8111
Cereri/sec: 1091.62
Transfer/sec: 32.27MB Prezența timeout-urilor sugerează o lipsă de resurse (am folosit 4 vCPU, 4 GB RAM gazde, Linux), iar de fapt potențialul RPS este mai mare (am obținut cifre de până la 2700 RPS pe resurse mai potente).
În concluzie, aș dori să menționez că tehnologia ESNI arată destul de promițător. Există încă multe întrebări nerezolvate, cum ar fi problemele legate de stocarea cheii publice ESNI în DNS și rotirea cheilor ESNI - aceste subiecte sunt discutate activ, iar ultima versiune a draftului (la momentul scrierii) ESNI este deja .
Sursa: habr.com
