Короткий ответ

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попадание в хром-лист, откат месяцами

Возможные причины

  1. HSTS включили, а 443 слушает не тот vhost / просроченный cert.
  2. includeSubDomains при живом dev.site.example на HTTP внутри LAN.
  3. preload отправили на hstspreload.org слишком рано.
  4. add_header в nginx в location без always — на 404 заголовка нет, на 200 есть: политика «мигает».
  5. Дубль HSTS от приложения и nginx с разными max-age.
  6. 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.example

Dry-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/ | head

HTTPS несёт ожидаемый 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

Неопределённо для клиента. Один.