Cześć Habr, nazywam się Ilia, pracuję w zespole platformowym firmy Exness. Tworzymy i wdrażamy podstawowe komponenty infrastrukturalne, które są wykorzystywane przez nasze zespoły deweloperskie.
W tym artykule chciałbym podzielić się doświadczeniem we wdrażaniu technologii encrypted SNI (ESNI) w infrastrukturze publicznych stron internetowych.

Zastosowanie tej technologii pozwoli zwiększyć poziom bezpieczeństwa podczas korzystania z publicznej strony internetowej oraz spełnić wewnętrzne standardy bezpieczeństwa przyjęte w firmie.
Przede wszystkim pragnę zwrócić uwagę, że technologia nie jest jeszcze standaryzowana i wciąż znajduje się w wersji roboczej, jednak CloudFlare i Mozilla już ją wspierają (w ). To nas zmotywowało do przeprowadzenia tego eksperymentu.
Trochę teorii
ESNI to rozszerzenie protokołu TLS 1.3, które pozwala na szyfrowanie SNI w wiadomości „Client Hello” podczas handshake TLS. Oto jak wygląda Client Hello z obsługą ESNI (zamiast zwykłego SNI widzimy ESNI):

Aby używać ESNI, potrzebne są trzy składniki:
- DNS;
- Wsparcie ze strony klienta;
- Wsparcie ze strony serwera.
DNS
Należy dodać dwa wpisy DNS – A, i TXT (Wpis TXT zawiera publiczny klucz, za pomocą którego klient może zaszyfrować SNI) – patrz poniżej. Ponadto konieczne jest wsparcie DoH (DNS przez HTTPS), ponieważ dostępni klienci (patrz poniżej) nie aktywują wsparcia ESNI bez DoH. To logiczne, ponieważ ESNI zakłada szyfrowanie nazwy zasobu, do którego się odwołujemy, więc bezsensowne jest korzystanie z DNS przez UDP. Co więcej, zastosowanie pozwala chronić przed atakami typu „cache poisoning” w tym scenariuszu.
Obecnie dostępnych jest , w tym:
CloudFlare (Sprawdź moją przeglądarkę → Encrypted SNI → Dowiedz się więcej), że ich serwery już teraz wspierają ESNI, a zatem dla serwerów CloudFlare w DNS mamy co najmniej dwa wpisy – A i TXT. W poniższym przykładzie żądamy Google DNS (przez HTTPS):
A wpis:
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 wpis, zapytanie jest formułowane według wzoru _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."
}Z perspektywy DNS musimy używać DoH (najlepiej z DNSSEC) i dodać dwa rekordy.
Wsparcie ze strony klienta
Jeśli mówimy o przeglądarkach, to w obecnej chwili . oto instrukcja, jak aktywować wsparcie ESNI i DoH w FireFoxie. Po skonfigurowaniu przeglądarki powinniśmy zobaczyć mniej więcej taki obrazek:

do weryfikacji przeglądarki.
Oczywiście, aby wspierać ESNI, musi być używany TLS 1.3, ponieważ ESNI to rozszerzenie do TLS 1.3.
W celach testowania backendu z wsparciem ESNI zrealizowaliśmy klienta na go, ale o tym za chwilę.
Wsparcie ze strony serwera
W chwili obecnej ESNI nie jest wspierany przez serwery WWW typu nginx/apache itp., ponieważ działają one na TLS za pośrednictwem OpenSSL/BoringSSL, w których ESNI nie jest oficjalnie wspierany.
Dlatego postanowiliśmy stworzyć nasz komponent front-endowy (ESNI reverse proxy), który wspierałby terminację TLS 1.3 z ESNI oraz proxy HTTP(S) ruchu do upstreamu, który nie wspiera ESNI. Umożliwia to zastosowanie technologii w już istniejącej infrastrukturze bez zmiany kluczowych komponentów – to znaczy można korzystać z obecnych serwerów WWW, które nie wspierają ESNI.
Dla przejrzystości przedstawiamy schemat:

Zaznaczam, że proxy zostało zaprojektowane z możliwością terminowania połączenia TLS bez ESNI, w celu wsparcia klientów bez ESNI. Również protokół komunikacji z upstreamem może być zarówno HTTP, jak i HTTPS z wersją TLS poniżej 1.3 (jeśli upstream nie wspiera 1.3). Taka konfiguracja daje maksymalną elastyczność.
Zrealizowane wsparcie ESNI na go pożyczono od . Od razu zaznaczam, że sama realizacja jest dość złożona, ponieważ wymaga zmian w standardowej bibliotece crypto/tls i dlatego wymaga "patchowania" GOROOT przed kompilacją.
Do generowania kluczy ESNI użyliśmy (też dzieło CloudFlare). Te klucze są używane do szyfrowania/odszyfrowania SNI.
Przetestowaliśmy kompilację używając go 1.13 na Linuxie (Debian, Alpine) oraz MacOS.
Kilka słów o cechach eksploatacyjnych
Proxy odwrotnego ESNI dostarcza metryki w formacie Prometheus, takie jak rps, opóźnienia upstream i kody odpowiedzi, nieudane/udane handshake TLS i czas handshake TLS. Na pierwszy rzut oka wydaje się to wystarczające do oceny, jak proxy radzi sobie z ruchem.
Przed użyciem przeprowadziliśmy również testy obciążeniowe. Wyniki poniżej:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Przeprowadzanie testu przez 6 minut @ https://esni-rev-proxy.npw:443
50 wątków i 1000 połączeń
Statystyki wątków Avg Stdev Max +/- Stdev
Latencja 1.77s 1.21s 7.20s 65.43%
Req/Sec 13.78 8.84 140.00 83.70%
206357 zapytań w 6.00m, 6.08GB odczytano
Zapytania/sec: 573.07
Transfer/sec: 17.28MB Testy obciążeniowe przeprowadziliśmy w sposób jakościowy, porównując schemat z wykorzystaniem proxy odwrotnego ESNI i bez niego. Wprowadziliśmy ruch lokalnie, aby wyeliminować zakłócenia w komponentach pośrednich.
Z obsługą ESNI i proxy na upstream z HTTP uzyskaliśmy około ~ 550 rps z jednego instancji, przy średnim zużyciu CPU/RAM proxy odwrotnego ESNI:
- 80% użycia CPU (4 vCPU, 4 GB RAM hosty, Linux)
- 130 MB Mem RSS

Dla porównania, RPS dla tego samego upstream nginx bez terminacji TLS (protokół HTTP) ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' --timeout 15s
Przeprowadzanie testu przez 6 minut @ http://lb.npw:80
50 wątków i 1000 połączeń
Statystyki wątków Avg Stdev Max +/- Stdev
Latencja 1.11s 2.30s 15.00s 90.94%
Req/Sec 23.25 13.55 282.00 79.25%
393093 zapytań w 6.00m, 11.35GB odczytano
Błędy socketów: connect 0, read 0, write 0, timeout 9555
Odpowiedzi nie 2xx lub 3xx: 8111
Zapytania/sec: 1091.62
Transfer/sec: 32.27MB Obecność timeoutów wskazuje, że brakuje zasobów (użyliśmy 4 vCPU, 4 GB RAM hosty, Linux), a potencjalny RPS jest wyższy (uzyskiwaliśmy liczby do 2700 RPS na mocniejszych zasobach).
Na zakończenie zauważam, że technologia ESNI wydaje się być dość obiecująca. Pozostaje wiele otwartych pytań, na przykład, dotyczących przechowywania publicznego klucza ESNI w DNS i rotacji kluczy ESNI - te kwestie są intensywnie dyskutowane, a ostatnia wersja szkicu (w momencie pisania) ESNI jest już .
Źródło: habr.com
