Как завершить TLS-соединение в Nginx
На днях поставили задачу: есть веб-балансировщик, обслуживающий несколько виртуальных хостов с доступом по HTTP и HTTPS. Требуется разрывать соединение с клиентами, подключающихся по TLS к списку сервисов, доступных только по HTTP. При этом пользователю запретить видеть предупреждения о несоответствии получаемого сертификата и FQDN.
В этом поможет расширение Server Name Indication (SNI) TLS. SNI предоставляет механизм передачи клиентом серверу информации о запрашиваемом хосте. Так веб-сервер узнаёт, какой сертификат ему следует использовать для соединения с виртуальным хостом.
Начиная с версии NGINX 1.11.5 доступен модуль ngx_stream_ssl_preread_module
. Модуль извлекает имя сервера, доступное через SNI на фазе ClientHello без терминирования SSL/TLS. Значение заголовка доступно в переменной $ssl_preread_server_name
. Используя её, можно динамически задавать адрес проксируемого TCP-бэкэнда:
stream {
proxy_protocol on;
upstream teardown {
server 127.0.0.1:444;
}
upstream backend {
server 127.0.0.1:445;
}
map $ssl_preread_server_name $name {
example.com teardown;
default backend;
}
server {
listen 444;
return "";
}
server {
listen 443;
ssl_preread on;
proxy_pass $name;
}
}
Так при обращении по HTTPS, NGINX перенаправит трафик в 445-й порт, а для example.com — в 444-й. В первом случае объявляем виртуальный хост и сертификат, во втором — веб-сервер разорвёт соединение.
Есть нюанс: используйте протокол PROXY для отображения реальных IP-адресов пользователей в логах. Например, для виртуального хоста example.com:
server {
listen 445 http2 proxy_protocol;
server_name example.com;
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
[...]
}
К сожалению, не выйдет рвать соединение для тех виртуальных хостов, где не указан SSL-сертификат. Как вариант, вместе с map
включать файл со списком разрешённых хостов. Такой список придётся создать вручную, а после отправить SIGHUP мастер-процессу NGINX для применения изменений.