Настройка заголовков безопасности в 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.
Не удаляйте политику целиком без необходимости.
Безопасный порядок настройки
- Создать резервную копию.
- Добавить
X-Content-Type-Options. - Добавить
Referrer-Policy. - Добавить
X-Frame-Options. - Добавить минимальную
Permissions-Policy. - Проверить сайт.
- Включить CSP в Report-Only.
- Исправить нарушения.
- Включить блокирующую CSP.
- Начать HSTS с малого
max-age. - Увеличить срок после проверки.
- Не добавлять
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 должны дополнять, а не заменять обновления, безопасную конфигурацию приложения и контроль доступа.