SNI
Server Name Indication — расширение TLS, которым клиент открытым текстом сообщает нужный домен, что позволяет держать много HTTPS-сайтов на одном IP.
Что такое SNI
SNI (Server Name Indication, «указание имени сервера») — расширение TLS, которым клиент в самом первом сообщении рукопожатия (ClientHello) сообщает, к какому домену он подключается. Передаётся до того, как согласовано шифрование, — то есть открытым текстом.
Проблема, которую SNI решает, — «курица и яйцо». На одном IP-адресе может жить сотня сайтов. По HTTP сервер узнаёт нужный из заголовка Host, но при HTTPS ему надо предъявить сертификат раньше, чем он увидит хоть один HTTP-заголовок: сертификат отдаётся в рамках рукопожатия, а заголовки идут уже внутри шифрованного канала. Без SNI сервер попросту не знает, чей сертификат отдавать. SNI приносит имя домена в первом же пакете.
Где SNI в рукопожатии
Клиент Сервер
│
│ ClientHello
│ server_name: api.example.com ← SNI, открытым текстом
│ supported_versions, cipher_suites
│──────────────────────────────────────────►│
│ │ выбирает server-блок
│ │ и нужный сертификат
│ ServerHello + Certificate (CN=api.example.com)
│◄──────────────────────────────────────────│
│ ... ключи согласованы, дальше всё зашифровано
SNI на практике: виртуальный хостинг
server {
listen 443 ssl;
server_name shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
}
server {
listen 443 ssl default_server; # сюда попадёт клиент без SNI или с чужим именем
server_name _;
return 444;
}
Оба сайта слушают один адрес и один порт — Nginx выбирает нужный блок по SNI. Без этого расширения каждому HTTPS-сайту требовался бы отдельный адрес, а IPv4 давно в дефиците.
Как посмотреть SNI
# какой сертификат сервер отдаст для конкретного имени
openssl s_client -connect 192.0.2.10:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates
# запрос на конкретный IP с нужным SNI и Host — до переключения DNS
curl -sv --resolve api.example.com:443:192.0.2.10 https://api.example.com/
Уберите -servername — и сервер отдаст сертификат из default_server. На этом ловится добрая половина жалоб вида «сертификат не тот»: клиент (старая библиотека, самописный скрипт, древний Java-клиент) не отправляет SNI, и сервер отвечает дефолтным блоком.
SNI виден всем на пути
Ключевое свойство: содержимое HTTPS зашифровано, а имя домена в ClientHello — нет. Любое устройство на маршруте — корпоративный шлюз, оборудование оператора, ТСПУ — видит, к какому домену идёт соединение, хотя ни URL, ни данных не видит.
На том же свойстве строятся полезные вещи. Балансировщик может маршрутизировать TCP по имени домена вообще без расшифровки — сертификаты остаются на бэкендах:
stream {
map $ssl_preread_server_name $backend {
api.example.com 10.0.0.11:8443;
shop.example.com 10.0.0.12:8443;
default 10.0.0.10:8443;
}
server {
listen 443;
ssl_preread on;
proxy_pass $backend;
}
}
ECH: шифрование SNI
Открытый SNI — известная утечка приватности: он раскрывает домен всем на пути. Закрывает её расширение ECH (Encrypted Client Hello) в TLS 1.3; более ранний черновик назывался ESNI. Идея: клиент берёт публичный ключ из DNS-записи (HTTPS RR), шифрует им внутренний ClientHello с настоящим именем, а снаружи остаётся имя-обёртка. Работает, только когда поддержку включили обе стороны — на практике в первую очередь крупные CDN, — и зависит от того, что DNS-запись получена без подмены, обычно через DoH.
Связанные термины
Готовы запустить GPU-задачу?
Запустить GPU-сервер