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.
  • Wildcard покрывает ровно один уровень и не покрывает сам домен - оба факта проверены.

Если знаешь, почему 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 в этом блоке не нужен, и это единственный случай, когда listen ... ssl без сертификата не считается ошибкой. Во всех остальных:

↳  выводnginx -t
[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-запроса, поэтому ssl_certificate задаётся в server, а блок выбирается теми же правилами приоритета, что и по Host. Неизвестное имя попадает в блок по умолчанию и получает чужой сертификат; закрывается это блоком с ssl_reject_handshake on, после которого клиент видит предупреждение TLS 112 и никакого сертификата - включая клиентов, которые SNI не отправляют вовсе. listen ssl без сертификата не даёт стартовать, кроме этого случая. Wildcard покрывает один уровень и не покрывает сам домен.

Комментарии

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

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

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