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

На https://site.example (10.0.20.40) добавьте заголовки на фронте nginx/Apache, не надеясь на плагин WP как единственный слой. Минимум: X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy по необходимости, Content-Security-Policy (начните с Content-Security-Policy-Report-Only), HSTS после стабильного HTTPS — подробно HSTS. X-Frame-Options устаревает в пользу frame-ancestors в CSP.

Не копируйте default-src 'none' с чужого блога на прод WordPress в пятницу вечером. Сначала report-only.

Не копируйте CSP default-src 'none' с чужого чеклиста на прод WordPress без недели Report-Only: сломаете админку и оплату. HSTS включайте только после рабочего 301 и certbot renew --dry-run. add_header в nginx location без always пропадает на 404 — проверяйте и ошибочные URL. Дубли заголовков из плагина WP и nginx уберите: оставьте один фронт. curl -sI на / и на заведомый 404 должны оба нести nosniff.

Симптомы и как отличить

  • Securityheaders / браузер: отсутствуют STS, CSP, nosniff.
  • Админка сломалась после CSP — слишком жёсткий script-src.
  • Mixed content — URL, не только CSP: mixed.

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

  1. HSTS на HTTP-vhost (браузер игнорирует) или при просроченном cert.
  2. CSP без 'unsafe-inline' на WP, где куча inline — админка мертва; лечите nonce/hash со временем, не 'unsafe-inline' навсегда без плана.
  3. X-Frame-Options DENY ломает легитимный iframe оплаты — используйте frame-ancestors точечно.
  4. Дубли от PHP и nginx.
  5. add_header без always — на 50x заголовков нет.
  6. server_tokens off путают с security headers — полезно, но это не CSP.

Диагностика

curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | grep -iE 'strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy|x-xss'
sudo nginx -T 2>/dev/null | grep -i add_header
sudo grep -R Header /etc/apache2/sites-enabled

Посмотрите отдельно /wp-admin/ — плагины могут добавить свои.

Решение

Снимок конфига:

sudo tar -C /etc -czf /root/nginx-headers-$(date +%F).tar.gz nginx

Базовый пакет nginx (server 443)

server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header X-Frame-Options "SAMEORIGIN" always;

X-XSS-Protection на современных браузерах не нужен (даже вреден). Не добавляйте «для галочки».

HSTS

Только когда 301 и certbot dry-run зелёные:

add_header Strict-Transport-Security "max-age=31536000" always;

Без preload в первый месяц. См. статью HSTS.

CSP: сначала Report-Only

add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;

Соберите нарушения в DevTools за неделю на витрине и в wp-admin. Затем переименуйте в Content-Security-Policy. WordPress почти наверняка потребует 'unsafe-inline' для style/script или nonce — фиксируйте исключения, не снимайте CSP целиком.

Для mixed полезен upgrade-insecure-requests в боевом CSP после правки своих URL.

Apache 2.4

Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000"

mod_headers. Header unset дублей от приложения при необходимости.

CORS не путать с CSP

Access-Control-Allow-Origin * на API — отдельное решение, не «безопасный заголовок по умолчанию». На витрине WP обычно не нужен *.

PHP не должен слать противоречие

Если плагин security дублирует STS с другим max-age — выключите заголовки в плагине, оставьте nginx.

Сценарий: WordPress, inline и nonce

Боевой CSP без 'unsafe-inline' на WP 6.x ломает админку: куча inline <script> и style. Путь: Report-Only неделю, собрать host-источники (*.google-analytics.com, CDN темы), затем script-src 'self' https://cdn.site.example 'unsafe-inline' как промежуточный этап. Цель — nonce: сложнее, делается в теме/му-плагине, не в этой статье целиком.

upgrade-insecure-requests в CSP не заменяет HSTS и не чинит mixed на чужих http-доменах. Связка: сначала URL в БД, потом CSP, потом HSTS.

Permissions-Policy режьте только то, что не нужно (камера/микрофон на витрине магазина одежды). Не копируйте interest-cohort=() как обязательный — заголовок эволюционирует, проверяйте актуальный синтаксис MDN на дату внедрения.

Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy для обычного WP с виджетами часто ломают оплату и карты. Не добавляйте COOP/COEP «пакетом hardening» без теста checkout.

Проверка 404:

curl -sI --resolve site.example:443:10.0.20.40 https://site.example/no-such | grep -iE 'x-content-type|strict-transport|content-security'

Если на 404 пусто — add_header без always или location 404 без пакета заголовков. Повторите директивы в error_page location.

Плагин «security headers» + nginx = дубли. Оставьте nginx, в плагине выключите отправку тех же имён.

Как проверить, что проблема устранена

curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | grep -iE 'x-content-type|strict-transport|referrer-policy|content-security'
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/missing | grep -i x-content-type

На 404 тоже nosniff, если стоит always. Откройте витрину и wp-admin: нет критичных CSP-блоков в консоли после перехода с Report-Only. HSTS только на HTTPS.

Проверьте встраивание, если бизнес вставляет сайт в iframe партнёра — frame-ancestors.

Если не помогло

  • Заголовки есть в curl, нет в браузере: CDN стрипает, настройте edge.
  • add_header пропал на PHP location — повтор пакета там.
  • Service Worker старый кэш.
  • Content-Type text/plain для JS — nosniff как раз блокирует, почините MIME application/javascript.

Профилактика

  • Заголовки в git-шаблоне vhost.
  • Report-Only на staging = прод.
  • После новых плагинов — повторный проход CSP.
  • Не мешать с бот-лимитами в одной «простыне» без комментариев.
  • Фиксируйте в git полный набор add_header одним сниппетом conf.d/headers.conf и include в server 443, чтобы location не теряли STS. На staging включайте Report-Only CSP той же строкой, что планируете в прод. Не возвращайте HPKP и не копируйте CSP с чужого блога в пятницу на прод.

FAQ

X-Frame-Options и CSP frame-ancestors вместе?

Можно на переходный период. При конфликте современные браузеры ориентируются на CSP. Не ставьте DENY и frame-ancestors https://partner одновременно без нужды.

Нужен ли Expect-CT?

Устарел. Не добавляйте.

Public-Key-Pins

Мёртв и опасен. Не используйте.

CSP unsafe-eval

Нужен некоторым сборщикам. Старайтесь убрать. Для WP сначала report.

Можно ли заголовки только на HTML?

Идеально map по $sent_http_content_type, сложнее в поддержке. Для малого сайта — на всех ответах origin допустимо, следите за статикой CDN (там свой набор).