Cum să îți protejezi site-ul public cu ESNI

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.

Cum să îți protejezi site-ul public cu ESNI

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 draft01). 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):

Cum să îți protejezi site-ul public cu 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 DNSSEC permite protecția împotriva atacurilor de tip „cache poisoning” în acest scenariu.

În prezent, sunt disponibili câțiva furnizori DoH, printre care:

CloudFlare declara (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 sprijinul este implementat doar în FireFox. Aici 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ă:

Cum să îți protejezi site-ul public cu ESNI

Link 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:

Cum să îți protejezi site-ul public cu ESNI

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 CloudFlare. 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 esnitool (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

Cum să îți protejezi site-ul public cu ESNI

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 7.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster