Когда на одном сервере живут несколько сайтов, удобно обойтись одним публичным IP-адресом и при этом раздавать «правильный» TLS-сертификат для каждого домена. Именно это и делает SNI (Server Name Indication): клиент сообщает имя хоста во время TLS-рукопожатия, и сервер выбирает подходящий сертификат ещё до того, как начнётся HTTP.
Ниже — как устроен SNI на практике, какие есть ограничения, и рабочие примеры конфигураций для Nginx, HAProxy (через crt-list) и Apache. В конце — диагностика: что проверять, если «настроил, а сертификат не тот».
Что такое SNI и почему без него было сложно
Исторически TLS-сервер должен был выбрать сертификат до того, как увидит HTTP-заголовок Host. Из-за этого классический вариант «несколько HTTPS-сайтов на одном порту» часто требовал несколько IP: сервер просто не знал, под какой домен пришло соединение.
SNI добавляет в ClientHello поле с именем (hostname). Сервер (или TLS-терминатор/прокси) сопоставляет это имя с виртуальным хостом и выбирает нужный сертификат. В результате схема становится простой: много доменов → один IP → один 443 → разные сертификаты.
Как это выглядит по шагам
- Клиент подключается к вашему IP:443.
- Отправляет
ClientHelloс SNI-именем (например,example.com). - Сервер/прокси выбирает сертификат для
example.comи продолжает TLS. - Только потом начинается HTTP и приходит
Host: example.com.
Ключевой момент: выбор сертификата происходит на уровне TLS, до HTTP. Поэтому «выбрать сертификат по URL» невозможно — только по имени хоста (SNI).
Ограничения и подводные камни SNI
SNI поддерживается практически всеми современными браузерами и библиотеками, но «краевые» проблемы всё ещё встречаются. Ниже — то, что чаще всего ломает ожидания.
1) Старые клиенты без SNI
Если клиент не отправляет SNI, сервер не понимает, какой домен нужен, и отдаёт «дефолтный» сертификат. Сегодня это редко для браузеров, но встречается в:
- старых встроенных устройствах;
- части кастомных HTTP-клиентов;
- устаревших Java/openssl-стэках (зависит от версий и сборок).
Практический вывод: держите дефолтный сертификат корректным и предсказуемым, а для критичных «несовместимых» клиентов иногда проще выделить отдельный IP.
2) Несовпадение SNI и HTTP Host
Иногда приходит одно имя в SNI, а в HTTP заголовке Host — другое (ошибка клиента, промежуточный прокси, криво настроенная миграция). На стороне сервера TLS-контекст уже выбран, и вы получаете:
- ошибки 400/421 (в зависимости от того, как настроена проверка и дефолтный обработчик);
- или попадание на «не тот» сайт, если дефолтный vhost делает редиректы или отдаёт контент.
Если у вас несколько уровней проксирования, лучше явно определять default-поведение на 443 и не пытаться «лечить» mismatch редиректами.
3) Цепочка сертификатов и порядок в файле
Одна из самых частых причин сообщений вида «incomplete chain»: сервер отдаёт leaf-сертификат, но не отдаёт промежуточные (intermediate) или они склеены в неправильном порядке. В типовом случае корректно так:
- сначала сертификат домена (leaf);
- затем промежуточные сертификаты (один или несколько);
- корневой сертификат обычно не добавляют (он есть в хранилище доверия у клиента).
Если вы используете коммерческие сертификаты, заранее проверьте, что отдаёте полный fullchain (для Nginx) или корректный единый PEM (для HAProxy), либо правильно подключаете chain в Apache. Если сертификата ещё нет, удобнее заранее подобрать подходящие SSL-сертификаты под все нужные имена (SAN/wildcard), чтобы не городить лишние vhost’ы и исключения.

Nginx: несколько сертификатов через server blocks
В Nginx SNI «встроен» в модель server-блоков. Достаточно описать разные server_name и указать соответствующие ssl_certificate и ssl_certificate_key.
Базовый пример на два домена
server {
listen 443 ssl;
server_name site-a.example;
ssl_certificate /etc/nginx/ssl/site-a/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/site-a/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8081;
}
}
server {
listen 443 ssl;
server_name site-b.example;
ssl_certificate /etc/nginx/ssl/site-b/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/site-b/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8082;
}
}
Дефолтный сертификат на случай клиентов без SNI
Чтобы контролировать поведение без SNI (или при неизвестном имени), заведите отдельный default-сервер. Именно он станет кандидатом на выдачу сертификата, если SNI отсутствует или не совпало ни с одним server_name.
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/default/privkey.pem;
return 444;
}
Код 444 специфичен для Nginx: соединение закрывается без ответа. Это удобно против мусорных запросов, но для некоторых бизнес-сценариев лучше вернуть 400/421 или отдать нейтральную страницу.
Перечитать сертификаты без остановки сервиса
nginx -t
nginx -s reload
HAProxy: SNI-терминация и crt-list
HAProxy часто ставят на «первый рубеж»: он принимает TLS, выбирает сертификат по SNI и проксирует запрос дальше. Для управления множеством сертификатов удобно использовать список — crt-list.
Как должен выглядеть PEM для HAProxy
Классический вариант для HAProxy — один PEM-файл, в котором подряд лежат приватный ключ, сертификат домена и промежуточные сертификаты. Если у вас ключ и сертификаты раздельно, их обычно склеивают в один файл в правильном порядке (leaf → intermediate).
Пример crt-list
Создадим файл /etc/haproxy/crt-list.txt:
/etc/haproxy/certs/site-a.pem site-a.example www.site-a.example
/etc/haproxy/certs/site-b.pem site-b.example
Первое поле — путь к PEM. Далее (через пробел) можно перечислять имена, для которых сертификат допустим. Это удобно для SAN и wildcard.
Фронтенд с выбором бэкенда по SNI
frontend fe_https
bind :443 ssl crt-list /etc/haproxy/crt-list.txt alpn h2,http/1.1
mode http
http-request set-header X-Forwarded-Proto https
use_backend be_site_a if { ssl_fc_sni -i site-a.example www.site-a.example }
use_backend be_site_b if { ssl_fc_sni -i site-b.example }
default_backend be_default
backend be_site_a
mode http
server s1 127.0.0.1:8081
backend be_site_b
mode http
server s1 127.0.0.1:8082
backend be_default
mode http
http-request deny deny_status 421
Маршрутизация по ssl_fc_sni часто надёжнее, чем по HTTP Host: к моменту HTTP TLS уже завершён, и сертификат выбран. Если у вас дальше цепочка прокси, следите, чтобы они корректно передавали Host и/или X-Forwarded-Host туда, где это важно для приложения.
Проверка и «мягкая» перезагрузка
haproxy -c -f /etc/haproxy/haproxy.cfg
Перезагрузку в продакшене обычно делают через systemd reload (если юнит настроен на seamless reload), чтобы не рвать активные соединения.
Apache: SNI через VirtualHost на 443
В Apache SNI реализован через name-based virtual hosts на *:443. Важно, чтобы был включён SSL-модуль и виртуальные хосты на TLS-порту описаны корректно.
Минимальный пример двух vhost’ов
<VirtualHost *:443>
ServerName site-a.example
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/site-a/cert.pem
SSLCertificateKeyFile /etc/apache2/ssl/site-a/privkey.pem
SSLCertificateChainFile /etc/apache2/ssl/site-a/chain.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8081/
ProxyPassReverse / http://127.0.0.1:8081/
</VirtualHost>
<VirtualHost *:443>
ServerName site-b.example
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/site-b/cert.pem
SSLCertificateKeyFile /etc/apache2/ssl/site-b/privkey.pem
SSLCertificateChainFile /etc/apache2/ssl/site-b/chain.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8082/
ProxyPassReverse / http://127.0.0.1:8082/
</VirtualHost>
Директивы могут отличаться в зависимости от версии Apache и вашей практики хранения цепочки. В некоторых сборках цепочку добавляют прямо в файл сертификата, и отдельный SSLCertificateChainFile не требуется.
Дефолтный VirtualHost
Apache выбирает «первый» vhost на *:443 как дефолтный. Поэтому для предсказуемости держите первым нейтральный vhost-заглушку или отдельный «default» сайт, чтобы без SNI не было сюрпризов.
Проверка: какой сертификат реально отдаётся по SNI
Проверять удобнее не браузером, а инструментами, где можно явно задать SNI. Самый универсальный вариант — openssl s_client.
OpenSSL s_client с явным SNI
openssl s_client -connect 203.0.113.10:443 -servername site-a.example -showcerts
Смотрите на:
- Subject/SAN у leaf-сертификата;
- цепочку (присутствуют ли intermediate);
- ALPN (предлагается ли h2).
Проверка «что будет без SNI»
openssl s_client -connect 203.0.113.10:443 -showcerts
Так вы увидите, какой сертификат выдаётся по умолчанию. Это важный тест и для старых клиентов, и для отлова «случайного default» в конфиге.
Если сертификат «не тот»: быстрый чеклист
- Кто у вас default на 443: в Nginx это
default_server, в Apache — первый vhost, в HAProxy — первый подходящий сертификат из bind/crt-list. - Совпадает ли имя: точное ли
server_name/ServerName, учтён лиwww, нет ли опечаток. - Один ли это адрес: нет ли разных
listen/bind/Listenна разных IP или в разных конфиг-файлах. - Цепочка: отдаётся ли intermediate и в правильном порядке.
- Применился ли конфиг:
nginx -t, проверка конфигурации HAProxy, reload/restart.
Где лучше завершать TLS: практический выбор
Выбор между Nginx/HAProxy/Apache как TLS-терминатором — это про архитектуру, а не про «как правильнее». Ориентируйтесь на место в цепочке и требования к маршрутизации.
TLS в Nginx
Хороший вариант для небольшого и среднего числа доменов: простая модель server-блоков, удобно рулить редиректами, заголовками и проксированием.
TLS в HAProxy
Подходит, когда нужен L7-балансировщик, много бэкендов и хочется маршрутизировать по SNI ещё до HTTP. Плюс удобно централизованно вести сертификаты через crt-list.
TLS в Apache
Рационально, если Apache уже основной веб-сервер или вам нужны его модули. При большом количестве сайтов конфигурации могут разрастаться, но принципы SNI остаются теми же.
Если вы планируете выносить TLS на отдельный слой (балансировщик/edge) или просто нужен изолированный сервер под проксирование и сертификаты, удобнее делать это на VDS: меньше ограничений по конфигам, портам и пакетам, чем на shared-окружении.

Типовые ошибки и как их избежать
Ошибка 1: «На одном домене всё ок, на другом отдаётся чужой сертификат»
Почти всегда причина одна из трёх:
- клиент без SNI (сверяйте тестом без
-servername); - дефолтный vhost/server перехватывает неизвестное имя;
- не учли фактическое имя (например, пользователи идут на
www, а вы настроили только корень).
Ошибка 2: «Сертификат правильный, но браузер пишет incomplete chain»
Проверьте цепочку. В Nginx обычно используют fullchain в ssl_certificate, в HAProxy — единый PEM с ключом и intermediate, в Apache — корректное подключение chain (или склейка, если так принято в вашей сборке).
Ошибка 3: «После обновления сертификата показывается старый»
- Убедитесь, что обновили именно тот файл, который указан в конфиге (не соседний, не symlink на старую цель).
- Сделайте reload, чтобы процесс перечитал сертификаты.
- Проверьте, нет ли перед вами второго TLS-терминатора (ещё один балансировщик/прокси), который продолжает отдавать старый сертификат.
Итоги
SNI — стандартный и надёжный способ обслуживать несколько HTTPS-доменов на одном IP. В Nginx это решается разными server-блоками с ssl_certificate, в HAProxy удобно вести сертификаты через crt-list и маршрутизировать по ssl_fc_sni, а в Apache SNI работает через VirtualHost на *:443.
Если выстроить понятный default-обработчик, следить за цепочками и проверять поведение через openssl s_client -servername, большинство «мистических» проблем исчезают ещё до продакшена.


