1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 3 из 6

SNI: чей сертификат отдавать

Коротко

Сертификат нужен раньше, чем придёт HTTP-запрос с заголовком Host. Имя сервера клиент присылает в самом начале рукопожатия - это SNI.

  • Блок выбирается по SNI теми же правилами, что и по Host: точное имя, маска, регулярка, default_server.
  • Неизвестное имя получает сертификат блока по умолчанию, то есть чужой. Это чинит ssl_reject_handshake on - клиент получает предупреждение TLS 112 unrecognized name и никакого сертификата.
  • listen ... ssl без сертификата не даёт nginx стартовать - кроме блока с ssl_reject_handshake.

Если знаешь, почему ssl_certificate живёт в server, и видел ssl_reject_handshake - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/conf.d/sites.confhttp
server {
    listen 443 ssl;
    server_name shop.local;
    ssl_certificate     /certs/shop.fullchain.crt;
    ssl_certificate_key /certs/shop.key;
}

server {
    listen 443 ssl;
    server_name blog.local;
    ssl_certificate     /certs/blog.fullchain.crt;
    ssl_certificate_key /certs/blog.key;
}

Кто-то направил на твой адрес домен unknown.local и открыл его. Какой сертификат он получит?

Сертификат shop.local - первого блока на порту. Имя не совпало ни с одним server_name, сработало правило третьего раздела: первый блок на паре «адрес и порт» становится блоком по умолчанию. Пользователь увидит предупреждение с чужим доменом, а сканер интернета - подсказку, какие сайты живут на этом адресе.

На что это похоже

Гардероб на входе в здание с несколькими организациями. Пальто принимают до того, как ты сказал, к кому идёшь, - значит, номерок надо выдать заранее.

Поэтому на входе спрашивают только одно: «в какую фирму?» Вопрос звучит раньше всего остального разговора, и от ответа зависит, к какой стойке тебя отправят. Не назвал фирму - отправят к первой по списку.

Механизм: имя приходит до запроса

ClientHello в нём SNI: shop.local сервер выбрал блок и отдал его сертификат GET / HTTP/1.1 только теперь приходит Host на момент выбора сертификата URI не существует - поэтому ssl_certificate живёт в server, а не в location
Между SNI и заголовком Host лежит всё рукопожатие. Совпадать они, кстати, не обязаны.

Правило

/etc/nginx/conf.d/00-default.confhttp
server {
    listen 443 ssl default_server;
    ssl_reject_handshake on;
}

Такой блок ставят первым файлом по алфавиту, и он обрывает рукопожатие для всех имён, не совпавших ни с одним server_name. Замер на стенде:

↳  выводopenssl s_client -connect shop.local:443 -servername unknown.local
406752FA427F0000:error:0A000458:SSL routines:ssl3_read_bytes:tlsv1 unrecognized name:../ssl/record/rec_layer_s3.c:1593:SSL alert number 112
no peer certificate available

Сертификата клиент не видит вовсе - значит, и чужого домена в предупреждении не будет, и сканеру нечего собирать.

ssl_certificate в этом блоке не нужен: рукопожатие будет отклонено. Для обычного TLS-сайта сертификат и ключ должны быть заданы здесь или унаследованы из http. Если их нет на обоих уровнях, проверка выдаст:

↳  выводфрагмент ошибки nginx -t
nginx: [emerg] no "ssl_certificate" is defined for the "listen ... ssl" directive
in /etc/nginx/conf.d/x.conf:1

Разбор: кто ещё попадёт под отказ

Тот же замер, но клиент вообще не отправил SNI:

↳  выводopenssl s_client -connect shop.local:443 (без -servername)
tlsv1 unrecognized name:../ssl/record/rec_layer_s3.c:1593:SSL alert number 112
no peer certificate available

Результат тот же. Это и есть цена решения: клиенты без SNI - очень старые браузеры, часть встроенных устройств, иногда самописные скрипты - перестанут открывать сайт совсем, а не увидят предупреждение. Сегодня таких почти нет, но если в требованиях есть древние клиенты, для такого сайта нужен отдельный адрес.

Ровно по этой же причине openssl s_client без -servername показывает не тот сертификат: он ничем не отличается от старого клиента.

Разбор: что покрывает wildcard

Стенд, сертификат на *.shop.local, три запроса:

адрес результат curl
a.shop.local 200
a.b.shop.local (60) SSL: no alternative certificate subject name matches target host name 'a.b.shop.local'
shop.local (60) SSL: no alternative certificate subject name matches target host name 'shop.local'

Звёздочка покрывает ровно один уровень и не покрывает сам домен. Поэтому в wildcard-сертификаты всегда включают голый домен отдельным именем, а не надеются на маску.

Что ломается без этого

Расхождение между тем, для чего выписан сертификат, и тем, что обслуживает блок, - самая частая причина предупреждений после добавления поддомена. Добавили api.shop.local в server_name, а сертификат перевыпустить забыли: имя совпало, блок выбран, сертификат отдан старый, браузер ругается.

Проверяется парой команд без всяких догадок:

$  команда
openssl x509 -in fullchain.pem -noout -ext subjectAltName
nginx -T | grep -A2 'server_name api'

Списки должны совпадать. И помни, что server_name и SAN - разные списки в разных местах: nginx про содержимое сертификата ничего не знает и на несоответствие не ругается.

Зачем это в работе

Блок с ssl_reject_handshake - две строки, которые стоит завести на любом сервере с публичным адресом. Он закрывает три вещи сразу: чужой домен в предупреждении пользователя, подсказку сканерам и случайное обслуживание трафика, направленного к тебе по ошибке или намеренно.

Проверить, что он работает, можно с любой машины:

$  команда
openssl s_client -connect shop.local:443 -servername чтоугодно.local </dev/null

Увидел alert number 112 - защита на месте. Увидел сертификат своего сайта - блок по умолчанию всё ещё отдаёт его кому попало.

Теперь сам

На сервере два сайта. Коллега добавил третий и написал в его блоке listen 443 ssl default_server;, потому что «так было в примере». Что изменилось для двух старых сайтов?

Для них по имени ничего: SNI совпадает, блоки выбираются точно. Изменилось поведение для неизвестных имён - теперь чужой сертификат отдаёт третий сайт, а не первый. А если флаг default_server уже стоял у другого блока на этом порту, nginx откажется собирать конфиг: два блока по умолчанию на одной паре «адрес и порт» - ошибка, разбирали в третьем разделе.

Главное

Для каждого имени проверь сертификат, выбранный по SNI, и отдельно запрос без известного имени. Сервер по умолчанию может предъявить сертификат рабочего сайта. Если это недопустимо, настрой отказ рукопожатия; редирект внутри HTTP уже не изменит ранее предъявленный сертификат.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.