10 сентября 2026 года в блоге nginx вышла заметка Spencer Ugbo: в NGINX Ingress Controller 5.6.0 HTTP Strict Transport Security вынесли в отдельный тип Policy для VirtualServer. Это не пакет nginx из Ubuntu и не CVE. Это CRD, который сам собирает заголовок Strict-Transport-Security, без snippets в манифесте.
Сам релиз 5.6, который это включил, описан там же 2 сентября 2026. Тогда HSTS стал first-class политикой на VirtualServer, VirtualServerRoute и Ingress. Раньше заголовок клали сниппетом: Kubernetes его не валидирует, ошибка доезжает до прода.
Откуда это известно
Первоисточник: knowledge-base пост nginx от 10 сентября. Автор: Spencer Ugbo. Разбор опирается на CRD Policy с spec.hsts и на привязку в VirtualServer.spec.policies.
Связанный пост того же домена: релиз NGINX Ingress Controller 5.6 от 2 сентября (Alessandro Fael Garcia). Там HSTS назван одной из причин уйти со snippets. В том же релизе ещё быстрее старт с -enable-config-safety, аннотации для миграции с Ingress-NGINX и правки Helm. Этот текст про HSTS, не дайджест всей 5.6.
Ссылку на docs.nginx.com из поста 10 сентября я не дублирую: на момент разбора якорь policy-resource/#hsts отдаёт 404. Рабочий пример, на который указывает блог: VirtualServer HSTS example в репозитории kubernetes-ingress.
Что это меняет на VPS с WordPress
Типичный WordPress на одном Ubuntu/Debian VPS с nginx, PHP-FPM и MariaDB этот CRD не применяет. NGINX Ingress Controller это контроллер в Kubernetes, не пакет nginx из репозитория дистрибутива. Если в systemctl status nginx обычный бинарник, а кластера нет, новость не про ваш стек.
HSTS на таком VPS по-прежнему живёт в конфиге сервера: заголовок отдаёт сам nginx, не Policy. Снимать его «удалением ресурса» там тоже нельзя: браузер помнит max-age, пока не получит новый заголовок. Это та же ловушка, только без CRD.
Если сайт реально стоит за NIC (VirtualServer или Ingress в кластере), 5.6.0 закрывает дыру процесса: HSTS становится ревьюируемым объектом, а не строкой в snippet. Для WordPress за таким ингрессом это значит: заголовок можно повесить на хост целиком, не трогая PHP-FPM и не смешивая его с CORS на конкретном route.
Как устроена Policy
Схема из блога короткая. Создаёте Policy с spec.hsts. Вешаете её на VirtualServer в spec.policies. Контроллер генерирует Strict-Transport-Security с теми директивами, которые задали.
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: hsts-policy
spec:
hsts:
maxAge: 31536000
includeSubDomains: false
behindProxy: false
preload: false
maxAge: 31536000 это год в секундах. Пример в блоге такой. Это не «магическое число nginx», это то, что уйдёт в заголовок.
includeSubDomains в примере выключен. Если включить, политика распространяется на все поддомены хоста: браузер будет требовать HTTPS и для api.example.com, и для app.example.com. На практике это бьёт по админкам, staging и почтовым веб-интерфейсам на соседних именах, если у них нет своего TLS.
behindProxy выключен: TLS должен терминироваться на самом Ingress Controller. Если включить, контроллер доверяет заголовку X-Forwarded-Proto, чтобы понять, что исходный запрос уже был HTTPS. Имеет смысл только когда перед NIC стоит балансировщик или CDN, который реально ставит этот заголовок. Слепо включать «на всякий случай» не стоит: подделанный X-Forwarded-Proto это отдельный класс ошибок.
preload сигнализирует, что домен хотят внести в HSTS preload lists браузеров. Тогда HTTPS требуют уже на первый визит, до любого заголовка. В блоге прямо: для preload нужны включённый includeSubDomains и maxAge не меньше 31536000 секунд. Это необратимее обычного HSTS. Для типичного WordPress на одном имени я бы не ставил preload, пока не решите, что HTTP вам больше не нужен ни на одном поддомене.
Куда вешать и чего Policy не прощает
Политики только на уровне сервера. Если повесить HSTS на route или subroute, Policy отклонят. Routes наследуют то, что висит в spec.policies.
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: webapp
spec:
host: webapp.example.com
tls:
secret: tls-secret
policies:
- name: hsts-policy
upstreams:
- name: webapp
service: webapp-svc
port: 80
routes:
- path: /
action:
pass: webapp
HSTS можно сочетать с другими политиками. В примере блога HSTS висит на spec, CORS на route. Контроллер обещает, что заголовки не разъедутся, даже если route добавляет свои.
На одном VirtualServer применяется только первая HSTS Policy. Остальные игнорируются. TLS обязателен, если behindProxy выключен. Заголовок уходит только в HTTPS-ответах: на голый HTTP его нет. Это штатное поведение, не баг конфига.
Как снять HSTS, чтобы не запереть браузер
Удалить Policy недостаточно. Браузер уже запомнил директиву и будет требовать HTTPS, пока не истечёт maxAge. Если за это время снимете TLS, часть клиентов просто не откроет сайт.
Порядок из блога:
- Обновить Policy:
maxAge: 0, применить. - Подождать, пока клиенты сходят на сайт и получат новый заголовок.
- Убрать ссылку на Policy из VirtualServer и удалить сам ресурс.
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: hsts-policy
spec:
hsts:
maxAge: 0
Про истечение в браузере блог отсылает к MDN: HSTS expiration. Клиент, который не заходил на сайт после maxAge: 0, так и останется на старом сроке. «Подождать» здесь не календарный день в вакууме, а визиты тех, кого заголовок уже коснулся.
Что проверить руками
Сначала понять, какой nginx перед сайтом. На одном VPS без Kubernetes:
nginx -v systemctl is-active nginx curl -sI https://example.com | grep -i strict-transport
Если заголовка нет, это не баг 5.6.0: пакет дистрибутива сам HSTS не включает. Если есть, смотрите add_header в конфиге виртуального хоста, не Policy. Перед любой правкой: копия файла. Проверка и применение:
sudo nginx -t sudo systemctl reload nginx
Restart «на всякий случай» здесь не нужен. reload после успешного nginx -t.
Если это NIC 5.6.0, смотреть версию контроллера, наличие Policy и то, что она висит на spec VirtualServer, не на route. Заголовок проверять по HTTPS имени из spec.host, не по ClusterIP. Ожидаемый кадр из блога: max-age совпадает с Policy, includeSubDomains и preload появляются только если их включили.
Имеет смысл отдельно проверить HTTP: заголовка там быть не должно. Если TLS терминирует прокси перед контроллером, сверка behindProxy и реального X-Forwarded-Proto, а не надежда, что «прокси сам разберётся».
Где обычно ломается
Путают NIC и nginx из apt. Обновили пакет на VPS, ждут Policy, её нет и не будет.
Вешают HSTS на route. В 5.6.0 это отклонят. Нужен spec VirtualServer.
Две HSTS Policy на одном хосте. Сработает первая, вторая молча игнорируется. Искать «почему не тот max-age» лучше в порядке ссылок, не в кэше браузера.
Включают includeSubDomains или preload, не закрыв TLS на соседних именах. Браузер запомнит это на год.
Сносят Policy и TLS в один день. Клиенты с живым max-age остаются на HTTPS, которого уже нет. Сначала maxAge: 0 и визиты, потом снятие.
На VPS правят конфиг и делают restart без nginx -t. Для HSTS это лишний даунтайм. Для опечатки в add_header это уже простой.
HSTS на одном имени не шифрует PHP-FPM и не закрывает MariaDB. Это заголовок браузеру, не замена сертификата Let’s Encrypt и не AppArmor. Сертификат по-прежнему certbot или acme.sh, срок смотреть отдельно. Перевыпускать всем «под HSTS» не нужно.
Релиз 5.6 ещё убрал поддержку интеграции NGINX Service Mesh: если в деплое остались её флаги или аннотации, их просят снять до апгрейда. Это из поста 2 сентября, не из разбора Policy. К HSTS не относится, но апгрейд 5.6 из-за этого может не подняться.
На одном VPS с WordPress я бы не тащил Kubernetes ради этой Policy. Если ингресса нет, достаточно заголовка в nginx и понимания, что max-age живёт в браузере дольше, чем строка в конфиге.