Безопасные заголовки в Nginx: CSP, HSTS и X-Frame-Options
Одна строка в конфигурации Nginx может закрыть сразу несколько классов атак, но неверный CSP способен сломать JavaScript, шрифты и оплату. Показываем рабочую схему настройки и проверки для Nginx 1.25.x.

Безопасные HTTP-заголовки в Nginx не требуют отдельного сервиса, но ошибка в одной директиве может либо оставить сайт уязвимым, либо отключить его JavaScript. Ниже — практическая схема для Nginx 1.25.x: как добавить CSP, HSTS и X-Frame-Options, проверить результат и не сломать сторонние скрипты, платёжные виджеты и поддомены.
Что именно защищают три заголовка
Эти заголовки решают разные задачи. CSP ограничивает источники, откуда браузер может загружать скрипты, стили, изображения и другие ресурсы. HSTS заставляет браузер обращаться к сайту только по HTTPS. X-Frame-Options запрещает или ограничивает встраивание страницы в iframe.
| Заголовок | От какой проблемы помогает | Главный риск неправильной настройки |
|---|---|---|
Content-Security-Policy | Выполнение внедрённого скрипта и загрузка ресурсов с непредусмотренных источников | Ломаются inline-скрипты, CDN, аналитика и виджеты |
Strict-Transport-Security | Переходы по HTTP после первого защищённого посещения | Поддомены без HTTPS перестают открываться у запомнивших правило клиентов |
X-Frame-Options | Встраивание страницы в чужой фрейм, включая clickjacking-сценарии | Перестаёт работать легитимный iframe на вашем или внешнем сайте |
Сноска. CSP (Content Security Policy) — политика безопасности контента: набор правил, который браузер получает от сервера и применяет к загрузке ресурсов и выполнению кода.
Безопасный заголовок — не тот, который просто присутствует в ответе. Он должен соответствовать реальным источникам ресурсов вашего сайта.
Базовая конфигурация для HTTPS-сервера
Размещайте эти директивы внутри того блока server, который обслуживает HTTPS. В примере CSP разрешает ресурсы с текущего домена, изображения через data:, а встраивание страницы — только на том же источнике.
server {
listen 443 ssl;
server_name example.com www.example.com;
# Пути к сертификатам и остальная настройка HTTPS опущены
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
root /var/www/example.com;
}
Параметр always заставляет Nginx добавлять заголовок не только к успешным ответам, но и к ответам с ошибками. Без него политика может отсутствовать, например, на странице ошибки, хотя именно такие ответы тоже видит браузер.
SAMEORIGIN разрешает встраивание только страницами того же источника. Если сайт нигде не должен появляться во фрейме, используйте более жёсткий вариант:
add_header X-Frame-Options "DENY" always;
Современный CSP-директивой для контроля фреймов считается frame-ancestors. В конфигурации выше она оставлена вместе с X-Frame-Options: старые браузеры могут понимать только второй заголовок, а новые — CSP.
Как настраивать CSP без случайного отключения сайта
Сначала соберите реальные источники
Откройте сайт в браузере и проверьте консоль разработчика. Ищите ошибки вида Refused to load the script, Refused to apply inline style или Refused to connect. В сообщении обычно указан источник, который не прошёл политику.
Составьте список отдельно для скриптов, стилей, изображений, шрифтов, запросов к API и фреймов. Не добавляйте в CSP весь домен через *, если можете указать конкретный источник. Иначе политика формально есть, но её защитная ценность резко снижается.
Скрипты и inline-код
Директива script-src 'self' разрешает внешние файлы с вашего источника, но запрещает inline-скрипты и обработчики вроде onclick. Это ожидаемое поведение, а не ошибка Nginx.
Если проект использует CDN или внешнюю аналитику, добавляйте конкретные домены, например:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.net https://analytics.example.net; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
Не добавляйте 'unsafe-inline' автоматически только ради исчезновения ошибок. Он разрешает inline-скрипты и заметно ослабляет защиту от XSS. Если inline-код необходим, безопаснее перейти на nonce или хэши, но их генерация и передача в шаблон требуют настройки приложения, а не одной строки в Nginx.
Подключаемые сервисы
Платёжная форма, карта, чат и аналитика могут использовать разные директивы. Скрипт загружается через script-src, запрос отправляется через connect-src, фрейм ограничивается frame-src, а сам сайт разрешает кому-то встраивать себя через frame-ancestors. Один домен в одной директиве не открывает автоматически остальные типы ресурсов.
Например, для внешнего API может понадобиться:
connect-src 'self' https://api.example.net;
Если вы не знаете точный набор источников, сначала используйте режим наблюдения:
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
В этом режиме браузер сообщает о нарушениях, но не блокирует ресурсы. Для полноценного сбора отчётов нужен настроенный endpoint приложения или отдельный сервис; просто добавленная директива не создаёт хранилище автоматически.
HSTS: включайте после проверки HTTPS
В примере используется max-age=31536000 — один год. После получения ответа по HTTPS браузер будет самостоятельно заменять обращения к этому домену по HTTP на HTTPS в течение указанного срока.
Не включайте includeSubDomains, пока не проверили каждый используемый поддомен. Старый тестовый хост, почтовый сервис или панель без TLS может стать недоступным для браузеров, которые уже получили HSTS. Директива не исправляет сертификат, редиректы и смешанный контент: она только задаёт правило для браузера.
Параметр preload добавляйте только после отдельной проверки требований сервиса предварительной загрузки HSTS. Удаление домена из такого списка не происходит мгновенно, поэтому это решение нельзя принимать как обычное изменение конфигурации.
Порядок проверки после изменения
- Проверьте синтаксис. Выполните
nginx -t. В ответе должны появиться сообщенияsyntax is okиtest is successful. - Перечитайте конфигурацию. Выполните
nginx -s reloadили используйте штатную команду перезагрузки вашего systemd-сервиса. Старые соединения не должны обрываться из-за самой операции reload. - Посмотрите заголовки. Выполните
curl -I https://example.com/. В ответе должны бытьContent-Security-Policy,Strict-Transport-SecurityиX-Frame-Options. - Проверьте ошибки в браузере. Откройте главную страницу, авторизацию, корзину, оплату и административный интерфейс. CSP-ошибки укажут, какой тип ресурса был заблокирован.
- Проверьте ответы с ошибками. Запросите несуществующий URL и убедитесь, что заголовки присутствуют и в ответе 404. Это проверяет, не забыли ли вы
always.
Что ломается чаще всего
- Заголовок виден не на всех маршрутах. В Nginx директивы
add_headerнаследуются от вышестоящего уровня только если на текущем уровне нет собственныхadd_header. Если в отдельномlocationуже добавлен другой заголовок, проверьте, не исчезли ли там CSP и HSTS. - CSP не разрешает inline-код. Это проявляется сообщением о заблокированном inline script или style. Перенесите код во внешний файл, используйте nonce или хэш, а не добавляйте широкое исключение без оценки риска.
- Сломалась сторонняя интеграция. Добавьте только фактически используемый домен в нужную директиву. Для API, фрейма и скрипта это могут быть разные правила.
- HSTS включили слишком рано. Если часть поддоменов ещё работает только по HTTP, не используйте
includeSubDomains. Сначала переведите их на HTTPS или исключите из общей схемы именования. - Заголовки дублируются. Проверьте, не добавляет ли их приложение, CDN или балансировщик. Две разные CSP-политики применяются одновременно и могут неожиданно ужесточить результат.
Где эта схема не подходит
Для приложения с большим количеством inline-кода базовый CSP из примера почти наверняка потребует доработки. Для сайта, который должен встраиваться на нескольких доверенных доменах, SAMEORIGIN будет слишком строгим: настройте frame-ancestors под реальный список источников и проверьте совместимость со старыми браузерами.
Если Nginx стоит за CDN или reverse proxy, конечный ответ должен проверяться снаружи, а не только на origin-сервере. Иначе вы можете увидеть корректную локальную конфигурацию, но получить другие заголовки на публичном домене.
Что делать дальше
Сначала включите CSP в режиме Report-Only и пройдите ключевые пользовательские сценарии. Затем исправьте источники, переведите политику в блокирующий режим и добавьте проверку заголовков в smoke-тест после каждого изменения Nginx, CDN или фронтенда. Отдельно измеряйте количество CSP-нарушений и проверяйте, не появились ли ошибки загрузки на страницах авторизации и оплаты.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: NGINX documentation: ngx_http_headers_module, MDN Web Docs: Content Security Policy
Частые вопросы
Можно ли добавить эти заголовки только в конфигурацию Nginx?
Почему после включения CSP перестал работать JavaScript?
Нужно ли указывать `includeSubDomains` в HSTS?
Достаточно ли X-Frame-Options для защиты от clickjacking?
Что делать, если у меня другая версия Nginx?
Почему заголовок есть на главной странице, но отсутствует на 404?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.