Короткий ответ
HSTS (Strict-Transport-Security) говорит браузеру: больше не ходи на site.example по HTTP, даже если пользователь ввёл http. Включайте, когда: сертификат валиден, цепочка полная, 301 с 80 работает, автопродление certbot проверено dry-run, поддомены, попавшие под includeSubDomains, умеют TLS. Origin 10.0.20.40. Начните с max-age=300 на неделю наблюдения, затем 31536000. Не ставьте preload в первый день.
Если HTTPS ещё падает — сначала сертификат и редирект, не HSTS.
HSTS не чинит просроченный сертификат: браузер просто чаще ходит на 443 и чаще показывает ошибку TLS.
Симптомы и как отличить
- После выключения HTTPS на тесте браузер всё равно бьёт в 443.
mail.site.exampleбез cert сломался послеincludeSubDomains.- Заголовка нет: политики нет, это не «неправильно», это «не включено».
- Mixed content — другая тема.
| Заголовок | Риск |
|---|---|
| нет STS | пользователи остаются на http |
| max-age=0 | сброс HSTS |
| includeSubDomains рано | боковые поддомены |
| preload | попадание в хром-лист, откат месяцами |
Возможные причины
- HSTS включили, а 443 слушает не тот vhost / просроченный cert.
includeSubDomainsпри живомdev.site.exampleна HTTP внутри LAN.preloadотправили на hstspreload.org слишком рано.add_headerв nginx вlocationбезalways— на 404 заголовка нет, на 200 есть: политика «мигает».- Дубль HSTS от приложения и nginx с разными max-age.
- CDN добавляет свой STS, origin другой.
Диагностика
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | grep -i strict-transport
curl -sI --resolve site.example:80:10.0.20.40 http://site.example/ | grep -i strict-transport
sudo nginx -T 2>/dev/null | grep -i strict-transport
sudo grep -R Strict-Transport /etc/apache2/sites-enabled /var/www/site.example 2>/dev/null | headПроверьте поддомены, которые реально используются: www, mail, npm панели.
echo | openssl s_client -connect 10.0.20.40:443 -servername site.example 2>/dev/null | openssl x509 -noout -dates -ext subjectAltName
sudo certbot renew --dry-run --cert-name site.exampleDry-run должен быть зелёный до большого max-age.
Решение
Сценарий A. Включение поэтапно (nginx)
Сначала короткий max-age, без preload, без includeSubDomains:
add_header Strict-Transport-Security "max-age=300" always;nginx -t && reload. Неделя метрик: нет массовых тикетов по поддоменам.
Затем:
add_header Strict-Transport-Security "max-age=31536000" always;Когда все поддомены на HTTPS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Только после осознанной заявки на preload:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;Требования preload (хромовский список) включают HTTPS на самом Apex и редирект, min max-age. Не копируйте preload из чужого сниппета «для галочки».
Сценарий B. Apache
Header always set Strict-Transport-Security "max-age=31536000"Модуль headers. Не в VirtualHost :80.
Сценарий C. Нужно «выключить» HSTS
Отдайте max-age=0 по HTTPS:
add_header Strict-Transport-Security "max-age=0" always;Клиенты, уже получившие большой max-age, сбросят после визита. Preload-список этим не очистить — отдельная процедура исключения.
Сценарий D. nginx add_header наследуется плохо
Если в location есть любой add_header, родительские пропадают. Повторите STS в том же location или используйте more_set_headers из пакета (если установлен), либо вынесите STS только в server без переопределения в location. Это частая «пропажа HSTS на главной / появление на /blog».
Сценарий E. WordPress тоже шлёт STS
Оставьте один источник — nginx. Плагины security дублируют и расходятся.
Сценарий F. Поддомены, почта и панели
includeSubDomains распространяется на mail.site.example, panel.site.example, dev.site.example. Если почта на отдельном хосте без валидного cert или панель NPM на HTTP :81 с именем в зоне — браузер после визита apex перестанет открывать их по http и может ругаться на cert. До includeSubDomains выпустите сертификаты на эти имена или вынесите панели на другой домен вне зоны (panel.example.net).
Preload: отправили форму — удаление заголовка не выкинет домен из Chrome сразу. Не включайте preload «чтобы SSL Labs был A+» в день выпуска первого LE. Сначала месяцы max-age без preload.
add_header ... always на 301 с 80 не нужен для STS (игнорируется). На 443 — нужен на 200 и на 404. Проверьте curl -sI https://site.example/no-page | grep -i strict.
Связка с редиректом: без 301 пользователь с чистым кэшем первый раз идёт на http; HSTS начинает работать со второго визита (или с preload).
Как проверить, что проблема устранена
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | grep -i strict-transport
curl -sI --resolve site.example:80:10.0.20.40 http://site.example/ | headHTTPS несёт ожидаемый max-age. HTTP — 301, без STS. Браузер в chrome://net-internals/#hsts (если пользуетесь) показывает домен после визита. Поддомены без TLS не должны входить в includeSubDomains, пока не готовы.
Сертификат не должен быть близок к expiry без рабочего renew.
Если не помогло
- Пользователи за корпоративным SSL-intercept: их MITM ломает HSTS pinning по-своему — не ваша отмена HSTS «для бухгалтерии».
- NPM на :81 HTTP: не вешайте includeSubDomains, пока панель не на отдельном имени вне зоны.
- После смены канона www↔apex HSTS на старом имени живёт в браузерах до max-age.
- Заголовок есть в
curlна/и нет на/blog/:add_headerв location блога перебил server. Повторите STS в этом location или уберите лишнийadd_headerоттуда.
Профилактика
- Чеклист: LE dry-run, 301, цепочка, мониторинг 443, потом STS.
- Документировать preload: кто отправил форму.
- STS в том же репозитории, что vhost, не «руками на проде в 2 ночи».
FAQ
Можно ли HSTS на IP 10.0.20.40?
Не нужно. Политика для DNS-имени. Сертификаты на raw IP обычно не LE.
max-age в минутах?
В секундах. 31536000 ≈ 365 дней.
Нужен ли HSTS для API?
Да, если API только HTTPS. Клиенты-не-браузеры заголовок могут игнорировать — всё равно держите только 443.
Отключает ли HSTS HTTP-01?
Нет на стороне CA: CA ходит по HTTP, это не браузер. Ломает людей, которые чинят ACME браузером по http — используйте curl.
Два заголовка STS
Неопределённо для клиента. Один.