# теория · шаг 2 из 5
HSTS и цена необратимости
Коротко
Редирект с http на https закрывает не всё: самый первый запрос всё равно уходит по http и его можно перехватить. Это и закрывает HSTS - способом, который трудно отменить.
- Браузер запоминает домен на
max-ageсекунд и дальше подставляетhttps://сам, ещё до отправки запроса. - Флаг
alwaysобязателен, иначе заголовок пропадёт с ответов 4xx и 5xx. - Свой
add_headerв дочернемlocationуносит HSTS целиком - проверено на стенде. includeSubDomainsпревращает обходимое предупреждение в жёсткий блок для всех поддоменов,preloadпрактически необратим.
Если знаешь, зачем always, и понимаешь цену includeSubDomains - листай до «Теперь сам».
Сначала ответь сам
add_header Strict-Transport-Security "max-age=31536000" always;
location /api/ {
add_header X-Request-Id $request_id always;
proxy_pass http://app:8000;
}
Заголовок объявлен на уровне server. Придёт ли HSTS на ответы /api/?
curl -sI https://shop.local/api/health | grep -iE 'strict|x-request'
X-Request-Id: 4f2c...
Нет: в выводе только X-Request-Id, строки Strict-Transport-Security нет.
Правило «всё или ничего» из второго раздела: свой add_header в блоке
отменяет весь унаследованный набор. HSTS тихо исчез там, где появилась одна
безобидная строка, и заметить это можно только запросом.
На что это похоже
Записка на двери подъезда: в этот дом ходят только через главный вход, боковая дверь больше не работает. Прочитал один раз - и целый год ходишь главным, даже не проверяя, открыта ли боковая.
Сильная сторона очевидна: злоумышленнику больше нечего перехватывать у боковой двери. Слабая - если ты повесил такую записку про весь квартал, а в соседнем доме главного входа нет, туда просто перестанут ходить.
Механизм: где именно закрывается дыра
Правило
add_header Strict-Transport-Security "max-age=31536000" always;
max-age - в секундах; 31536000 это 365 × 24 × 60 × 60, ровно год. Три вещи,
из которых состоит вся тема:
alwaysобязателен. Без него заголовок не попадёт на ответы с кодами ошибок - замер из седьмого раздела показывает, что 403 и 500 теряют его, а 200 и 302 нет.- Ставят только в https-блоке. nginx честно отправит заголовок и по http (проверено: он там есть), но браузер по спецификации его проигнорирует - так задумано, иначе кто угодно на пути мог бы включить тебе HSTS на чужой домен.
- Срок отсчитывается заново с каждым ответом. Пока пользователь заходит, год не кончается.
Разбор: два параметра, меняющие цену ошибки
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
includeSubDomains распространяет правило на все поддомены сразу - включая те, о
которых ты забыл. Если у www.shop.local, dev.shop.local или old.shop.local
нет валидного сертификата, они перестанут открываться совсем: предупреждение,
которое можно было пропустить кнопкой «всё равно перейти», превращается в блок
без обхода.
preload - заявка на попадание в списки, вшитые в браузеры. После неё домен
ходит по https даже при первом в жизни визите, но выйти из списка сложно и
долго: обновление приезжает с новой версией браузера, у части пользователей -
через месяцы.
Порядок включения, который экономит нервы:
| шаг | что делаем | цена отката |
|---|---|---|
| 1 | max-age=300 |
один reload |
| 2 | проверяем сертификат у всех поддоменов, включая www |
- |
| 3 | поднимаем до max-age=31536000 |
до года у уже зашедших |
| 4 | добавляем includeSubDomains |
то же, но на все поддомены |
| 5 | preload |
месяцы и заявка на исключение |
Разбор: полный http-блок
server {
listen 80;
server_name shop.local www.shop.local;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name shop.local www.shop.local;
ssl_certificate /etc/letsencrypt/live/shop.local/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.local/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
root /var/www/shop;
index index.html;
}
Три вещи из трёх глав раздела, собранные вместе: заголовок только в https-блоке,
редирект внутри location / (а не на уровне server - иначе уедет челлендж),
проверка ACME отдаётся с диска по http.
Что ломается без этого
Самый обидный случай - HSTS уже разошёлся по браузерам, а сертификат протух или сломался. Обычный сайт в этой ситуации показывает предупреждение с кнопкой «всё равно перейти»; сайт с HSTS не показывает ничего, кроме глухой ошибки. Времени на починку у тебя ровно столько, сколько пользователи готовы ждать.
Отсюда и порядок из таблицы: пока max-age маленький, ошибка стоит одного
reload. После preload цена ошибки измеряется месяцами - и это единственная
настройка во всём курсе, которую нельзя откатить своими силами.
Второе, что ломается тихо, - заголовок, съеденный наследованием. Проверяется одной командой:
curl -sI https://shop.local/api/health | grep -i strict
Пусто там, где ожидался заголовок, - ищи ближайший add_header уровнем ниже.
Лечится дублированием строки или add_header_inherit merge из версии 1.29.3.
Зачем это в работе
HSTS - редкий случай, когда правильная настройка делается за минуту, а неправильная стоит недели. Поэтому её включают не «заодно с https», а отдельным шагом, когда всё остальное уже работает: сертификаты продлеваются автоматически, поддомены проверены, мониторинг на срок стоит.
И проверяют, что заголовок реально доезжает до всех типов ответов:
for p in / /api/health /nosuchpage; do curl -sI https://shop.local$p | grep -ci strict; done
Три единицы - всё в порядке. Ноль хотя бы в одной строке - есть блок, где заголовок потерялся.
Теперь сам
Сайт год отдавал max-age=31536000; includeSubDomains. Появился новый поддомен
metrics.shop.local на внутреннем сервере с самоподписанным сертификатом.
Сотрудники не могут его открыть: браузер не показывает кнопку «всё равно
перейти». Как быстро починить?
Быстро - никак: правило уже лежит в браузерах сотрудников и живёт год с
последнего визита на основной домен. Убрать includeSubDomains с боевого сайта
поможет только тем, кто зайдёт на него снова, и не сразу. Рабочий выход - выдать
поддомену нормальный сертификат от того же центра. Каждому вручную можно
почистить запись в настройках браузера, но это не решение для парка машин.
Главное
HSTS закрывает дыру первого http-запроса: браузер запоминает домен на max-age
секунд и сам ходит по https. Ставят его только на https-ответы и обязательно с
always, иначе заголовок исчезает на ошибках; свой add_header в дочернем
location уносит его целиком. includeSubDomains превращает обходимое
предупреждение в жёсткий блок для всех поддоменов, preload практически
необратим - поэтому начинают с max-age=300 и поднимают срок, лишь убедившись,
что сертификаты в порядке у всех имён.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий