Ciao Habr, sono Ilya, lavoro nel team di piattaforma di Exness. Sviluppiamo e implementiamo componenti infrastrutturali di base utilizzati dai nostri team di sviluppo prodotto.
In questo articolo vorrei condividere l'esperienza di implementazione della tecnologia SNI crittografato (ESNI) nell'infrastruttura dei siti web pubblici.

L'uso di questa tecnologia migliorerà il livello di sicurezza nella gestione di un sito web pubblico e consentirà di rispettare gli standard di sicurezza interni adottati dall'azienda.
Innanzitutto, voglio sottolineare che la tecnologia non è standardizzata e si trova ancora in fase di bozza, tuttavia CloudFlare e Mozilla la supportano già (in ). Questo ci ha motivato a condurre un esperimento simile.
Un po' di teoria
ESNI è un'estensione del protocollo TLS 1.3 che consente di crittografare l'SNI nel messaggio «Client Hello» del handshake TLS. Ecco come appare il Client Hello con supporto ESNI (invece del tradizionale SNI vediamo ESNI):

Per utilizzare ESNI, sono necessari tre elementi:
- DNS;
- Supporto da parte del client;
- Supporto da parte del server.
DNS
È necessario aggiungere due record DNS – A, e TXT (La registrazione TXT contiene la chiave pubblica che il cliente può utilizzare per crittografare SNI) – vedi sotto. Inoltre, deve esserci supporto DoH (DNS over HTTPS), poiché i client disponibili (vedi sotto) non attivano il supporto ESNI senza DoH. Questo ha senso, poiché ESNI implica la crittografia del nome della risorsa a cui ci stiamo collegando, quindi non ha senso interrogare il DNS tramite UDP. Inoltre, l'uso della permette di proteggersi da attacchi di «cache poisoning» in questo scenario.
Attualmente sono disponibili , tra cui:
CloudFlare (Check My Browser → Encrypted SNI → Learn More) che i loro server già supportano ESNI, quindi per i server CloudFlare in DNS abbiamo almeno due registrazioni – A e TXT. Nell'esempio seguente, stiamo interrogando Google DNS (over HTTPS):
E registrazione:
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 registrazione, la richiesta è formata secondo il modello _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."
}Quindi, in termini di DNS, dovremmo utilizzare DoH (preferibilmente con DNSSEC) e aggiungere due record.
Supporto da parte del client
Se parliamo di browser, attualmente . È fornita una guida su come attivare il supporto per ESNI e DoH in FireFox. Una volta configurato il browser, dovremmo vedere un quadro simile a questo:

per verificare il browser.
Naturalmente, per supportare ESNI deve essere utilizzato TLS 1.3, poiché ESNI è un'estensione di TLS 1.3.
Per testare il backend con supporto ESNI, abbiamo implementato un client in go, ma di questo parleremo più tardi.
Supporto da parte del server
Attualmente, ESNI non è supportato dai server web come nginx/apache, ecc., poiché funzionano con TLS tramite OpenSSL/BoringSSL, nei quali ESNI non è ufficialmente supportato.
Per questo motivo, abbiamo deciso di creare il nostro componente front-end (proxy inverso ESNI) che supporti la terminazione TLS 1.3 con ESNI e il proxying del traffico HTTP(S) verso un upstream che non supporta ESNI. Questo permette di applicare la tecnologia in un'infrastruttura già esistente, senza modificare i componenti principali, ovvero utilizzare i server web attuali che non supportano ESNI.
A scopo dimostrativo, riportiamo lo schema:

Va notato che il proxy è stato concepito per terminare la connessione TLS senza ESNI, per supportare i clienti senza ESNI. Inoltre, il protocollo di comunicazione con l'upstream può essere sia HTTP che HTTPS con una versione TLS inferiore alla 1.3 (se l'upstream non supporta la 1.3). Questo schema offre la massima flessibilità.
L'implementazione del supporto ESNI su go l'abbiamo presa da . Sottolineo subito che l'implementazione stessa è piuttosto non banale, in quanto implica modifiche alla libreria standard crypto/tls e quindi richiede un 'patching' del GOROOT prima della compilazione.
Per generare le chiavi ESNI abbiamo utilizzato (anch'esso creato da CloudFlare). Queste chiavi sono utilizzate per la crittografia/decrittografia del SNI.
Abbiamo testato la compilazione utilizzando go 1.13 su Linux (Debian, Alpine) e MacOS.
Una parola sulle caratteristiche operative
Il proxy inverso ESNI fornisce metriche nel formato Prometheus, come rps, latenza upstream & codici di risposta, handshake TLS riusciti/falliti & durata dell'handover TLS. A prima vista, ciò sembrava sufficiente per valutare come il proxy gestisce il traffico.
Abbiamo anche effettuato test di carico prima dell'uso. I risultati sono riportati di seguito:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Esecuzione del test di 6m @ https://esni-rev-proxy.npw:443
50 thread e 1000 connessioni
Statistiche sui thread Avg Stdev Max +/- Stdev
Lat. 1.77s 1.21s 7.20s 65.43%
Req/Sec 13.78 8.84 140.00 83.70%
206357 richieste in 6.00m, 6.08GB letti
Richieste/sec: 573.07
Trasferimenti/sec: 17.28MB Abbiamo condotto i test di carico puramente qualitativi, per confrontare gli schemi usando il proxy inverso ESNI rispetto a senza. Abbiamo "iniettato" traffico localmente per escludere "interferenze" negli elementi intermedi.
Quindi, con il supporto ESNI e il proxying verso l'upstream tramite HTTP, abbiamo ottenuto circa ~ 550 rps da un'istanza, con un consumo medio di CPU/RAM del proxy inverso ESNI:
- 80% di utilizzo della CPU (4 vCPU, host da 4 GB di RAM, Linux)
- 130 MB Mem RSS

A titolo di confronto, RPS per lo stesso upstream nginx senza terminazione TLS (protocollo HTTP) è ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' –-timeout 15s
Running 6m test @ http://lb.npw:80
50 thread e 1000 connessioni
Statistiche sui thread Media Deviazione Massimo +/- Deviazione
Latenza 1.11s 2.30s 15.00s 90.94%
Richieste/sec 23.25 13.55 282.00 79.25%
393093 richieste in 6.00m, 11.35GB letti
Errori di socket: connessione 0, lettura 0, scrittura 0, timeout 9555
Risposte non-2xx o 3xx: 8111
Richieste/sec: 1091.62
Trasferimento/sec: 32.27MB La presenza di timeout indica una mancanza di risorse (abbiamo utilizzato host con 4 vCPU e 4 GB di RAM, Linux), e di fatto il potenziale RPS è più alto (abbiamo ottenuto cifre fino a 2700 RPS su risorse più potenti).
In conclusione, segnalo che la tecnologia ESNI appare piuttosto promettente. Ci sono ancora molte questioni aperte, come la memorizzazione della chiave ESNI pubblica nel DNS e la rotazione delle chiavi ESNI – queste questioni sono attivamente discusse, e l'ultima versione della bozza (al momento della scrittura) di ESNI è già .
Fonte: habr.com
