Глеб Гончаров

Сисадмин и девопс из Ульяновска

Принудительно завершаем 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 для применения изменений.