Короткий ответ
На 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.
Возможные причины
- HSTS на HTTP-vhost (браузер игнорирует) или при просроченном cert.
- CSP без
'unsafe-inline'на WP, где куча inline — админка мертва; лечите nonce/hash со временем, не'unsafe-inline'навсегда без плана. X-Frame-Options DENYломает легитимный iframe оплаты — используйтеframe-ancestorsточечно.- Дубли от PHP и nginx.
add_headerбезalways— на 50x заголовков нет.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-Typetext/plainдля JS — nosniff как раз блокирует, почините MIMEapplication/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 (там свой набор).