Как защитить свой публичный сайт с ESNI

Привет Хабр, меня зовут Илья, я работаю в платформенной команде компании Exness. Мы разрабатываем и внедряем базовые инфраструктурные компоненты, которые используют наши продуктовые команды разработки.

В этой статье я бы хотел поделиться опытом внедрения технологии encrypted SNI (ESNI) в инфраструктуре публичных веб-сайтов.

my3vbqy87t_ep9l7wnjzsghs9oo.png
Использование этой технологии позволит повысить уровень безопасности при работе с публичным веб-сайтом и соответствовать внутренним стандартам безопасности, принятым в Компании.

Прежде всего, хочу обратить внимание, что технология не стандартизована и все еще находится в драфте, однако CloudFlare и Mozilla уже поддерживают ее (в draft01). Это и мотивировало нас на такой эксперимент.

Немного теории


ESNI — это расширение к протоколу TLS 1.3, которое позволяет шифровать SNI в сообщении «Client Hello» TLS handshake.  Вот, как выглядит Client Hello с поддержкой ESNI (вместо привычного SNI мы видим ESNI):

74defd4491b50c470ee3b81651edfb62.png

 Чтобы использовать ESNI, необходимы три составляющие:

  • DNS;  
  • Поддержка со стороны клиента;
  • Поддержка со стороны сервера.


DNS


Необходимо добавить две DNS записи — A, и TXT (TXT запись содержит публичный ключ, с помощью которого клиент может зашифровать SNI) — см. ниже. Кроме того, должна быть поддержка DoH (DNS over HTTPS), так как доступные клиенты (см. ниже) не активируют поддержку ESNI без DoH. Это логично, так как ESNI подразумевает шифрацию имени ресурса, к которому мы обращаемся, то есть бессмысленно обращаться к DNS по UDP. Более того,   использование DNSSEC позволяет защититься от «cache poisoning» атак в этом сценарии.

На текущий момент доступно несколько DoH провайдеров, среди них:


CloudFlare заявляет (Check My Browser → Encrypted SNI → Learn More), что их серверы уже сейчас поддерживают ESNI, то есть для серверов CloudFlare в DNS мы имеем как минимум две записи — А и TXT. В примере ниже мы запрашиваем Google DNS (over HTTPS):  

А запись:

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 запись, запрос формируется по шаблону _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."
}


Итак, с точки зрения DNS, мы должны использовать DoH (желательно с DNSSEC) и добавить две записи. 

Поддержка со стороны клиента


Если мы говорим о браузерах, то на сегодняшний момент поддержка реализована только в FireFox. Здесь приведена инструкция, как активировать поддержку ESNI и DoH в FireFox. После того, как браузер настроен, мы должны увидеть примерно такую картину:

f066efb4ce235c0abf798652123b1a13.png

Ссылка для проверки браузера.

Разумеется, для поддержки ESNI должен быть использован TLS 1.3, так как ESNI — это расширение к TLS 1.3.

Для целей тестирования бэкенда с поддержкой ESNI мы реализовали клиента на go, но об этом чуть позже.

Поддержка со стороны сервера


На текущий момент ESNI не поддерживается web-серверами типа nginx/apache и т.д., так как они работают с TLS посредством OpenSSL/BoringSSL, в которых ESNI официально не поддерживается.

Поэтому мы решили создать свой front-end компонент (ESNI reverse proxy), который бы поддерживал терминацию TLS 1.3 с ESNI и проксирование HTTP (S) траффика на апстрим, не поддерживающий ESNI. Это позволяет применять технологию в уже сложившейся инфраструктуре, без изменения основных компонентов — то есть использовать текущие web-серверы, не поддерживающие ESNI. 

Для наглядности приведем схему:

b30035725aca99c4c0085c237a1d1a9e.png

Отмечу, что прокси задумывался с возможностью терминировать TLS соединение без ESNI, для поддержки клиентов без ESNI. Также, протокол общения с апстримом может быть как HTTP, так и HTTPS c версией TLS ниже 1.3 (если апстрим не поддерживает 1.3). Такая схема дает максимальную гибкость.

Реализацию поддержки ESNI на go мы позаимствовали у CloudFlare. Сразу отмечу, что сама реализация достаточно нетривиальная, так как подразумевает изменения в стандартной библиотеке crypto/tls и поэтому требует «патчинга» GOROOT перед сборкой.

Для генерации ESNI ключей мы использовали esnitool (тоже детище CloudFlare). Данные ключи используются для шифрации/дешифрации SNI.
Мы протестировали сборку с использованием go 1.13 на Linux (Debian, Alpine) и MacOS. 

Пара слов об эксплуатационных особенностях


ESNI reverse proxy предоставляет метрики в формате Prometheus, например, такие, как rps, upstream latency & response codes, failed/successful TLS handshakes & TLS handshake duration. На первый взгляд это показалось достаточным для оценки того, как прокси справляется с трафиком. 

Также перед использованием мы провели нагрузочное тестирование. Результаты ниже:

wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Running 6m test @ https://esni-rev-proxy.npw:443
  50 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.77s     1.21s    7.20s    65.43%
    Req/Sec    13.78      8.84   140.00     83.70%
  206357 requests in 6.00m, 6.08GB read
Requests/sec:    573.07
Transfer/sec:     17.28MB 


Нагрузочное тестирование мы проводили чисто качественное, для сравнения схемы с использованием ESNI reverse proxy и без. Мы «наливали» трафик локально для того, чтобы исключить «помехи» в промежуточных компонентах.

Итак, с поддержкой ESNI и проксированием на апстрим с HTTP,   мы получили в районе ~ 550 rps с одного инстанса, при этом среднее потребление CPU/RAM ESNI reverse proxy:

  • 80% CPU Usage (4 vCPU, 4 GB RAM хосты, Linux)
  • 130 MB Mem RSS


lxademanjtgdgtbp2zj3dtztoaa.png

Для сравнения, RPS для того же апстрима nginx без терминации TLS (HTTP протокол) ~ 1100:

wrk -t50 -c1000 -d360s 'http://lb.npw:80' –-timeout 15s
Running 6m test @ http://lb.npw:80
  50 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.11s     2.30s   15.00s    90.94%
    Req/Sec    23.25     13.55   282.00     79.25%
  393093 requests in 6.00m, 11.35GB read
  Socket errors: connect 0, read 0, write 0, timeout 9555
  Non-2xx or 3xx responses: 8111
Requests/sec:   1091.62
Transfer/sec:     32.27MB 


Наличие таймаутов говорит о том, что есть нехватка ресурсов (мы использовали 4 vCPU, 4 GB RAM хосты, Linux), и по факту потенциальный RPS выше (мы получали цифры до 2700 RPS на более мощных ресурсах).

В заключение отмечу, что технология ESNI выглядит достаточно перспективно. Есть еще много открытых вопросов, например, вопросы хранения публичного ESNI ключа в DNS и ротирование ESNI-ключей — эти вопросы активно обсуждаются, а последняя версия драфта (на момент написания) ESNI уже 7.

© Habrahabr.ru