Один порт — много секретов: SNI-демультиплексор на nginx
Классическая задача: на сервере один внешний адрес, а за ним — и публичный сайт,
и то, что должно быть видно только своим. Открывать по порту на каждый сервис —
получается «новогодний календарь» для сканера. Решение, которым я давно пользуюсь:
stream-блок nginx с раскладкой по SNI ещё до установления TLS.
Схема
stream {
map $ssl_preread_server_name $backend {
site.example.com 127.0.0.1:8443; # сайт: терминируем
svc1.example.com 127.0.0.1:9101; # сервис: passthrough
default 127.0.0.1:8443;
}
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
}
}
Когда passthrough лучше терминирования
Если сервис за раскладкой сам умеет TLS и сам отвечает за свой сертификат — не трогай его трафик. Passthrough сохраняет оригинальное рукопожатие: клиент видит чужой сертификат ровно такой, каким его отдал сервис. Любая твоя подмена — заметная аномалия для того, кто смотрит.
Терминирование оправдано, когда нужно смотреть внутрь (логи, модификация заголовков) или делить один сертификат между бэкендами. Но за каждое «посмотреть внутрь» платишь тем, что теперь и ты отвечаешь за этот трафик.
Грабли
Первое: ssl_preread читает только первый ClientHello. Если клиент
шлёт GREASE или разбитые пакеты — nginx всё равно справится, но старые сборки
падали на этом, обновляйтесь. Второе: не забудьте proxy_protocol
согласовать на обоих концах, если пробрасываете адрес клиента — иначе бэкенд
увидит 127.0.0.1 во всех логах, и вы потеряете возможность банить по IP.
И главное правило: сканер, постучавший на 443 без SNI, должен получать ровно то же, что получает на обычном сайте. Тогда порт выглядит скучно. Скучный порт — живой порт.