Настройка заголовков безопасности в Nginx

Пошаговая настройка базовых HTTP-заголовков безопасности в Nginx с проверкой через curl и безопасным применением.

HTTP-заголовки безопасности помогают браузеру строже обрабатывать содержимое сайта и снижают риск отдельных классов атак.

В этой инструкции рассматривается только настройка базовых заголовков безопасности в Nginx:

  • X-Content-Type-Options;
  • X-Frame-Options;
  • Referrer-Policy;
  • Permissions-Policy;
  • Strict-Transport-Security;
  • базовая Content-Security-Policy.

Проверено на: Ubuntu Server 24.04 LTS
Уровень сложности: средний
Время выполнения: около 15–20 минут
Требуемый доступ: пользователь с правами sudo

Что будет настроено

После выполнения инструкции:

  • будут добавлены базовые security headers;
  • будет настроен HSTS для HTTPS;
  • будет добавлена ограниченная Content Security Policy;
  • будет выполнена проверка через curl;
  • будут разобраны риски неправильной CSP и HSTS;
  • будет показан безопасный порядок применения.

Важное предупреждение

Некоторые заголовки могут нарушить работу сайта.

Особенно осторожно применяйте:

Strict-Transport-Security
Content-Security-Policy
Permissions-Policy

Перед изменением:

  • создайте резервную копию конфигурации;
  • проверьте сайт в тестовой среде;
  • не включайте HSTS до стабильной работы HTTPS;
  • не включайте includeSubDomains, если не все поддомены работают по HTTPS;
  • не включайте preload без отдельной проверки;
  • внедряйте CSP поэтапно.

Проверка текущих заголовков

curl -I https://example.com

Для подробного вывода:

curl -sS -D - -o /dev/null   https://example.com

Проверьте, какие security headers уже присутствуют.

Создание резервной копии

sudo cp   /etc/nginx/sites-available/example.com   /etc/nginx/sites-available/example.com.before-security-headers

Проверьте:

ls -l   /etc/nginx/sites-available/example.com*

Где размещать заголовки

Для одного сайта директивы удобно добавлять внутрь соответствующего блока:

server {
    listen 443 ssl;
    server_name example.com;

    # Заголовки безопасности
}

Не добавляйте глобальные заголовки в nginx.conf, если разные сайты требуют разных политик.

X-Content-Type-Options

Добавьте:

add_header X-Content-Type-Options "nosniff" always;

Этот заголовок запрещает браузеру самостоятельно менять MIME-тип ответа.

Проверка:

curl -I https://example.com |
grep -i x-content-type-options

Ожидаемый результат:

X-Content-Type-Options: nosniff

X-Frame-Options

Для полного запрета отображения сайта во frame:

add_header X-Frame-Options "DENY" always;

Если сайт должен открываться во frame только с того же origin:

add_header X-Frame-Options "SAMEORIGIN" always;

Не используйте одновременно DENY и SAMEORIGIN.

Когда X-Frame-Options может помешать

Заголовок может нарушить работу:

  • embedded dashboard;
  • iframe-интеграции;
  • внутренних порталов;
  • систем предпросмотра;
  • виджетов.

Перед включением проверьте, используется ли сайт внутри iframe.

Referrer-Policy

Разумный базовый вариант:

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Эта политика:

  • сохраняет полный URL для переходов внутри сайта;
  • передаёт только origin при переходе на другой HTTPS-сайт;
  • не передаёт referrer при переходе с HTTPS на HTTP.

Проверка:

curl -I https://example.com |
grep -i referrer-policy

Permissions-Policy

Минимальная политика:

add_header Permissions-Policy   "camera=(), microphone=(), geolocation=()"   always;

Она запрещает использование:

  • камеры;
  • микрофона;
  • геолокации.

Если приложению нужны эти функции, настройте политику точечно.

Пример разрешения geolocation только своему origin

add_header Permissions-Policy   'geolocation=(self), camera=(), microphone=()'   always;

Проверяйте синтаксис в браузере: поддержка отдельных директив может отличаться.

Strict-Transport-Security

HSTS сообщает браузеру, что сайт должен открываться только по HTTPS.

Добавьте в HTTPS server block:

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

Это запоминает HTTPS-политику на один год.

Когда нельзя включать HSTS

Не включайте HSTS, если:

  • HTTPS работает нестабильно;
  • сертификат может истечь;
  • сайт иногда должен открываться по HTTP;
  • поддомены ещё не готовы к HTTPS;
  • используется временный тестовый домен;
  • не проверено автоматическое продление сертификата.

После получения HSTS браузер будет принудительно использовать HTTPS до окончания max-age.

includeSubDomains

Добавлять:

includeSubDomains

можно только если все поддомены поддерживают HTTPS.

Пример:

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

Если хотя бы один поддомен работает только по HTTP, он станет недоступен для браузеров, запомнивших HSTS.

preload

Не добавляйте:

preload

без отдельной подготовки.

HSTS preload означает включение домена в браузерный preload list и требует:

  • HTTPS на основном домене;
  • HTTPS на всех поддоменах;
  • includeSubDomains;
  • длительного max-age;
  • постоянной готовности инфраструктуры.

Откат из preload занимает время.

Базовая Content-Security-Policy

Начальная строгая политика для простого статического сайта:

add_header Content-Security-Policy   "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

Эта политика:

  • разрешает ресурсы только с текущего origin;
  • запрещает object;
  • ограничивает base;
  • запрещает встраивание страницы во frame.

Почему CSP нужно внедрять осторожно

Строгая CSP может заблокировать:

  • внешние шрифты;
  • CDN;
  • inline JavaScript;
  • inline CSS;
  • analytics;
  • iframe;
  • API-запросы;
  • WebSocket;
  • изображения с внешних доменов.

Не копируйте чужую CSP без анализа ресурсов сайта.

CSP для сайта с внешними изображениями

Пример:

add_header Content-Security-Policy   "default-src 'self'; img-src 'self' https: data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

CSP для reverse proxy приложения

Приложения часто используют:

  • inline scripts;
  • WebSocket;
  • внешние API;
  • CDN;
  • data URI;
  • blob URI.

Для них CSP нужно строить по фактическим ресурсам приложения.

Не добавляйте 'unsafe-inline' и 'unsafe-eval' без необходимости.

Content-Security-Policy-Report-Only

Для безопасного теста используйте:

add_header Content-Security-Policy-Report-Only   "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

В этом режиме браузер сообщает о нарушениях, но не блокирует ресурсы.

После проверки замените:

Content-Security-Policy-Report-Only

на:

Content-Security-Policy

Готовый базовый блок

Для обычного HTTPS-сайта:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy   "camera=(), microphone=(), geolocation=()"   always;
add_header Strict-Transport-Security   "max-age=31536000"   always;
add_header Content-Security-Policy   "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

Полный пример server block

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com;

    ssl_certificate         /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key         /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com/html;
    index index.html;

    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy         "strict-origin-when-cross-origin"         always;
    add_header Permissions-Policy         "camera=(), microphone=(), geolocation=()"         always;
    add_header Strict-Transport-Security         "max-age=31536000"         always;
    add_header Content-Security-Policy         "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"         always;

    location / {
        try_files $uri $uri/ =404;
    }
}

Зачем используется always

Без параметра:

always

Nginx добавляет заголовок не ко всем кодам ответа.

С always заголовки будут присутствовать также при:

  • 403;
  • 404;
  • 500;
  • redirect;
  • других ответах.

Наследование add_header

В Nginx директивы add_header наследуются только если на текущем уровне нет собственных add_header.

Пример:

server {
    add_header X-Frame-Options "DENY" always;

    location /api/ {
        add_header Cache-Control "no-store";
    }
}

В таком location заголовок X-Frame-Options может не унаследоваться.

Поэтому внимательно проверяйте итоговые headers для каждого location.

Использование snippet-файла

Создайте:

sudo nano   /etc/nginx/snippets/security-headers.conf

Добавьте:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy   "strict-origin-when-cross-origin"   always;
add_header Permissions-Policy   "camera=(), microphone=(), geolocation=()"   always;
add_header Strict-Transport-Security   "max-age=31536000"   always;

Подключите в HTTPS server block:

include /etc/nginx/snippets/security-headers.conf;

CSP лучше оставлять в конфигурации конкретного сайта, потому что она зависит от приложения.

Проверка конфигурации

sudo nginx -t

Ожидаемый результат:

syntax is ok
test is successful

Применение

sudo systemctl reload nginx

Проверьте:

systemctl status nginx --no-pager

Проверка всех заголовков

curl -sS -D - -o /dev/null   https://example.com

Фильтрация:

curl -sS -D - -o /dev/null   https://example.com |
grep -iE   'content-security-policy|strict-transport-security|x-content-type-options|x-frame-options|referrer-policy|permissions-policy'

Проверка redirect-ответа

curl -I http://example.com

Если redirect выполняется отдельным HTTP server block, HSTS там не нужен: он учитывается браузером только при HTTPS-ответе.

Проверка 404

curl -I   https://example.com/missing-page

Убедитесь, что благодаря always заголовки присутствуют и в ответе 404.

Проверка через браузер

Откройте Developer Tools:

Network → Document → Response Headers

Проверьте каждый заголовок.

Для CSP дополнительно откройте:

Console

Ошибки CSP будут отображаться в консоли браузера.

Проверка CSP без блокировки

Сначала используйте:

Content-Security-Policy-Report-Only

Проверьте все страницы сайта:

  • главную;
  • авторизацию;
  • формы;
  • загрузку файлов;
  • JavaScript;
  • WebSocket;
  • внешние шрифты;
  • изображения;
  • iframe;
  • административную панель.

Только после этого включайте блокирующую CSP.

Проверка внешних ресурсов

Откройте исходный HTML:

curl -s https://example.com |
grep -Eo   '(src|href)="[^"]+"' |
head -100

Проверьте домены внешних ресурсов.

Для динамического приложения используйте Network tab браузера.

Проблема с inline JavaScript

Строгая CSP блокирует:

<script>
    console.log("test");
</script>

Безопасные варианты:

  • вынести скрипт в отдельный файл;
  • использовать nonce;
  • использовать hash;
  • изменить шаблоны приложения.

Не добавляйте 'unsafe-inline' как постоянное решение без анализа.

Проблема с inline CSS

Строгая CSP может блокировать:

<style>
    body { ... }
</style>

Решения:

  • вынести CSS в отдельный файл;
  • использовать nonce;
  • использовать hash;
  • точечно настроить style-src.

Пример CSP для внешнего CDN

add_header Content-Security-Policy   "default-src 'self'; script-src 'self' https://cdn.example.net; style-src 'self' https://cdn.example.net; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

Разрешайте только конкретные доверенные домены.

CSP для WebSocket

Пример:

add_header Content-Security-Policy   "default-src 'self'; connect-src 'self' wss://app.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"   always;

Без connect-src WebSocket или API-запросы могут блокироваться.

X-Frame-Options и frame-ancestors

Современная CSP использует:

frame-ancestors

Она гибче, чем X-Frame-Options.

Для совместимости можно использовать оба:

add_header X-Frame-Options "DENY" always;

и:

frame-ancestors 'none'

Неактуальные заголовки

Не рекомендуется добавлять без необходимости:

X-XSS-Protection
Public-Key-Pins
Expect-CT

Причины:

  • устарели;
  • не поддерживаются современными браузерами;
  • могут создавать ложное ощущение защиты;
  • HPKP особенно опасен при ошибочной настройке.

Server header

По умолчанию Nginx может показывать версию в ошибках и заголовках.

В /etc/nginx/nginx.conf внутри http:

server_tokens off;

Это скрывает версию Nginx, но не делает сервер защищённым само по себе.

Проверка:

curl -I https://example.com |
grep -i '^server:'

Создание резервной копии snippet

sudo cp   /etc/nginx/snippets/security-headers.conf   /etc/nginx/snippets/security-headers.conf.backup-$(date +%F-%H%M%S)

Откат заголовков

Удалите или закомментируйте проблемную директиву.

Проверьте:

sudo nginx -t

Примените:

sudo systemctl reload nginx

Для HSTS локальный откат конфигурации не отменяет уже сохранённую браузером политику до окончания max-age.

Как безопасно тестировать HSTS

Начните с небольшого значения:

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

Это 5 минут.

После проверки увеличьте:

3600
86400
31536000

Не начинайте сразу с длительного max-age на неподготовленном домене.

Типичные проблемы

Заголовок не отображается

Проверьте:

  • активный server block;
  • уровень размещения add_header;
  • наличие других add_header в location;
  • параметр always;
  • итоговую конфигурацию.

Команда:

sudo nginx -T |
grep -n 'add_header'

CSP ломает сайт

Переведите её в:

Content-Security-Policy-Report-Only

Изучите Console браузера и разрешите только необходимые источники.

Сайт нельзя открыть в iframe

Проверьте:

X-Frame-Options
frame-ancestors

Измените политику только для доверенного origin.

После HSTS браузер не открывает HTTP

Это ожидаемо.

Браузер автоматически переключает запрос на HTTPS.

Поддомен перестал открываться после includeSubDomains

На поддомене должен быть рабочий HTTPS.

Убрать includeSubDomains из Nginx недостаточно для браузеров, уже запомнивших политику, до окончания max-age.

Permissions-Policy блокирует нужную функцию

Разрешите функцию только нужному origin.

Не удаляйте политику целиком без необходимости.

Безопасный порядок настройки

  1. Создать резервную копию.
  2. Добавить X-Content-Type-Options.
  3. Добавить Referrer-Policy.
  4. Добавить X-Frame-Options.
  5. Добавить минимальную Permissions-Policy.
  6. Проверить сайт.
  7. Включить CSP в Report-Only.
  8. Исправить нарушения.
  9. Включить блокирующую CSP.
  10. Начать HSTS с малого max-age.
  11. Увеличить срок после проверки.
  12. Не добавлять includeSubDomains и preload без отдельного аудита.

Быстрый набор директив

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy   "strict-origin-when-cross-origin"   always;
add_header Permissions-Policy   "camera=(), microphone=(), geolocation=()"   always;
add_header Strict-Transport-Security   "max-age=31536000"   always;

Быстрая проверка

sudo nginx -t &&
sudo systemctl reload nginx
curl -sS -D - -o /dev/null   https://example.com |
grep -iE   'content-security-policy|strict-transport-security|x-content-type-options|x-frame-options|referrer-policy|permissions-policy'

Итог

После выполнения инструкции:

  • добавлены базовые HTTP-заголовки безопасности;
  • настроены ограничения iframe;
  • настроена политика referrer;
  • ограничены browser permissions;
  • рассмотрено безопасное включение HSTS;
  • добавлена и проверена CSP;
  • разобраны наследование add_header и типичные ошибки.

Security headers должны дополнять, а не заменять обновления, безопасную конфигурацию приложения и контроль доступа.

← Предыдущая статья Ограничение доступа по IP в Nginx Следующая статья → Ограничение размера загружаемых файлов в Nginx