Заголовки безопасности HTTP — это одна из тех вещей, которые большинство разработчиков откладывают "на потом", а потом сайт взламывают или он падает по безопасности в аудите. Я за 10+ лет насмотрелся на это столько раз, что решил написать подробное руководство — с реальными конфигами, примерами из практики и объяснением того, зачем вообще всё это нужно.
Зачем вообще нужны заголовки безопасности
Честно говоря, когда я начинал работать с вебом, про security headers почти никто не говорил. Делаешь сайт, ставишь SSL — и молодец. Но мир изменился. Браузеры стали умнее, атаки стали сложнее, а Google и Яндекс теперь реально смотрят на безопасность при ранжировании.
Заголовки безопасности — это HTTP-заголовки, которые сервер отправляет браузеру вместе с ответом. Браузер читает их и понимает, как себя вести: откуда можно грузить ресурсы, разрешать ли встраивание в iframe, как обрабатывать MIME-типы и так далее. Грубо говоря, это инструкции для браузера на тему "что делать можно, а что — нет".
Был у меня случай в 2023 году: клиент с интернет-магазином на Битрикс жаловался, что пользователи видят чужую рекламу прямо на его сайте. Оказалось — XSS-атака через форму обратной связи. Злоумышленники внедряли скрипты, которые подгружали сторонние баннеры. Банальный Content-Security-Policy закрыл бы эту дыру полностью. После настройки заголовков — проблема исчезла. Клиент потерял несколько продаж за те дни, пока мы разбирались. Мог не терять вообще.
Ещё один момент: инструменты вроде securityheaders.com и Google Lighthouse открыто показывают оценку по заголовкам. У клиентов, которые ко мне приходят на поддержку Битрикс, я регулярно вижу оценку F или D. После правильной настройки — стабильно A или A+. Это влияет и на доверие пользователей, и на SEO.
Content-Security-Policy: самый важный заголовок
CSP — это, пожалуй, самый мощный и одновременно самый сложный заголовок из всех. Он позволяет явно указать, откуда браузер может загружать скрипты, стили, изображения, шрифты и прочие ресурсы. Всё остальное — блокируется.
Я написал отдельную подробную статью про это — Content Security Policy: настройка и защита сайта 2026. Там разобраны все директивы с примерами. Здесь дам базовую конфигурацию для nginx, которую я использую как стартовую точку:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-RANDOM_NONCE' https://www.google-analytics.com https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://www.google-analytics.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
Но здесь важно понимать: CSP — это не "поставил и забыл". Каждый сайт уникален. Если у вас Bitrix с кучей виджетов, встроенным чатом и Яндекс.Метрикой — придётся аккуратно прописывать каждый источник. Сначала я всегда рекомендую запустить в режиме Content-Security-Policy-Report-Only. Это позволяет собирать нарушения в лог, не блокируя ничего реально. Потом анализируешь отчёты и строишь финальную политику.
Strict-Transport-Security: принудительный HTTPS
HSTS — это заголовок, который говорит браузеру: "Этот сайт всегда работает по HTTPS. Больше не проверяй HTTP-версию." После первого посещения браузер сохраняет эту инструкцию и в течение указанного времени (max-age) автоматически переключает все запросы на HTTPS, даже если пользователь вводит адрес без "https://".
Про настройку редиректов и HSTS я подробно писал в статье Настройка HTTPS редиректов и HSTS: полное руководство 2026. Но вот базовая конфигурация для nginx:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Параметр max-age=31536000 — это 1 год в секундах. includeSubDomains распространяет правило на все поддомены. А preload — это флаг для включения в глобальный список HSTS Preload List. Но вот тут стоп.
Preload — это серьёзный шаг. Если вы добавите домен в preload-список, а потом захотите вернуться на HTTP (например, у вас истёк сертификат или что-то пошло не так) — удалить домен из списка займёт недели, а то и месяцы. У одного моего клиента была такая история: включили preload, потом сменили хостинг, новый хостинг не поддерживал автопродление Let's Encrypt, сертификат протух — и сайт стал полностью недоступен на несколько дней, пока не разобрались. Так что preload — только если вы на 100% уверены, что HTTPS у вас навсегда.
Начинайте с небольшого max-age — например, max-age=86400 (1 день). Убедитесь, что всё работает. Потом увеличивайте до 2592000 (30 дней), потом до 31536000. Постепенно.
X-Frame-Options и защита от кликджекинга
Кликджекинг — это атака, при которой злоумышленник встраивает ваш сайт в прозрачный iframe на своей странице и обманом заставляет пользователя кликать не туда, куда он думает. Например, жертва думает, что нажимает на безобидную кнопку, а на самом деле кликает по кнопке "Перевести деньги" на вашем банковском сайте.
Заголовок X-Frame-Options решает эту проблему. У него три варианта:
DENY— запрещает встраивание в iframe вообще вездеSAMEORIGIN— разрешает встраивание только с того же доменаALLOW-FROM uri— разрешает встраивание с конкретного URL (устарело, не поддерживается в современных браузерах)
На практике я почти всегда использую DENY для обычных сайтов и SAMEORIGIN для тех, где нужны внутренние iframe (например, некоторые компоненты Битрикс используют iframe для административных форм). Но тут важный нюанс: X-Frame-Options — устаревший заголовок. Современный способ — директива frame-ancestors в CSP. Тем не менее, для совместимости со старыми браузерами стоит оставлять оба.
# В nginx
add_header X-Frame-Options "SAMEORIGIN" always;
# Если используете CSP — дублируйте там же
# frame-ancestors 'self';
Для Apache (.htaccess) это выглядит так:
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)"
</IfModule>
X-Content-Type-Options: остановить MIME-сниффинг
Этот заголовок — один из самых простых, но при этом реально важных. Значение у него одно: nosniff. И он запрещает браузеру "угадывать" тип контента, если сервер указал один MIME-тип, а содержимое файла выглядит иначе.
Зачем это нужно? Представьте: злоумышленник загружает на ваш сайт файл с расширением .jpg, но внутри там JavaScript. Без nosniff старые браузеры могли "понять", что это скрипт, и выполнить его. С заголовком — браузер доверяет тому, что сказал сервер, и не пытается угадать.
Ставить этот заголовок нужно всегда. Без исключений. Это буквально одна строка в конфиге, которая закрывает целый класс атак:
add_header X-Content-Type-Options "nosniff" always;
Referrer-Policy и Permissions-Policy
Referrer-Policy
Когда пользователь кликает по ссылке с вашего сайта на другой, браузер отправляет заголовок Referer с адресом страницы, с которой пришёл пользователь. Это может быть проблемой, если в URL есть конфиденциальные данные — например, токены сессии, параметры поиска или личные данные.
Referrer-Policy позволяет контролировать, что именно будет передаваться. Вариантов много, но я на практике использую два основных:
strict-origin-when-cross-origin— отправляет полный URL при переходах внутри домена, только origin (без пути) при переходах на другие домены по HTTPS, и ничего при переходе с HTTPS на HTTP. Это хороший баланс безопасности и функциональности.no-referrer— вообще не отправляет Referer. Максимальная приватность, но может сломать аналитику.
Для большинства сайтов я рекомендую strict-origin-when-cross-origin. Это значение стало дефолтным в современных браузерах, так что даже если вы не выставите заголовок явно — браузер скорее всего будет вести себя именно так. Но лучше указать явно, чтобы не зависеть от дефолтов конкретного браузера.
Permissions-Policy
Раньше этот заголовок назывался Feature-Policy. Он управляет доступом к браузерным API: камере, микрофону, геолокации, платёжным системам и так далее. Если ваш сайт не использует камеру — зачем вообще давать к ней доступ? Закройте явно.
add_header Permissions-Policy "
camera=(),
microphone=(),
geolocation=(self),
payment=(),
usb=(),
accelerometer=(),
gyroscope=()
" always;
Синтаксис немного изменился в 2023-2024 году. Старый формат был geolocation=(self), новый — geolocation=(self). Они совпадают в этом случае, но для некоторых директив синтаксис отличается. Проверяйте актуальную документацию MDN, потому что браузеры периодически меняют поддержку.
Настройка в разных окружениях: nginx, Apache, PHP
На деле я работаю с тремя основными сценариями: nginx как основной сервер, Apache с .htaccess (чаще всего на shared-хостингах), и PHP-уровень (когда нет доступа к конфигу сервера). Разберём каждый.
Полная конфигурация nginx
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL конфигурация
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# Заголовки безопасности
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com;" always;
# Убираем версию сервера из заголовков
server_tokens off;
root /var/www/example.com;
index index.php index.html;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Обратите внимание на server_tokens off — это убирает версию nginx из заголовка Server. Злоумышленники часто сначала смотрят на версию сервера, чтобы найти известные уязвимости. Незачем им это показывать.
Настройка через PHP
Иногда нет доступа к конфигу nginx или Apache. Например, на некоторых shared-хостингах или в проектах, где нужно управлять заголовками динамически. В этом случае можно выставлять заголовки прямо в PHP. Я обычно делаю это в файле bootstrap.php или аналогичном месте, которое выполняется при каждом запросе:
<?php
// Только для HTTPS
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=(self)');
// CSP — настройте под ваш проект
$csp = implode('; ', [
"default-src 'self'",
"script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'"
]);
header("Content-Security-Policy: {$csp}");
// Убираем PHP-версию из заголовков
header_remove('X-Powered-By');
Последняя строка — header_remove('X-Powered-By') — убирает заголовок, который по умолчанию PHP добавляет с указанием версии. Это та же история, что и server_tokens off в nginx. Зачем показывать, что у вас PHP 8.2? Но лучше это делать через php.ini: expose_php = Off.
functions.php / мю-плагин. Для Битрикс — через .htaccess или nginx-конфиг. Подробнее о защите WordPress читайте в статье Как защитить сайт от взлома: 10 правил безопасности.Заголовки, которые нужно убрать
Настройка безопасности — это не только добавление нужных заголовков, но и удаление лишних. Некоторые заголовки по умолчанию раскрывают информацию о вашем сервере, которая может помочь злоумышленникам.
Что убираем:
Server— показывает тип и версию веб-сервера (nginx/1.24.0, Apache/2.4.54). Используйтеserver_tokens offв nginx илиServerTokens Prod+ServerSignature Offв Apache.X-Powered-By— показывает PHP версию и иногда CMS. Отключается черезexpose_php = Offв php.ini илиheader_remove('X-Powered-By')в PHP.X-AspNet-Version— если у вас .NET (маловероятно для читателей этого блога, но всё же).
В nginx убрать заголовок Server полностью (не просто скрыть версию) можно только с модулем nginx_headers_more:
more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';
Но на практике server_tokens off достаточно. Знание того, что у вас nginx — это не критическая информация. Критично — знать версию с известными уязвимостями.
Проверка и мониторинг заголовков
После настройки обязательно проверяйте результат. Я использую несколько инструментов одновременно:
- securityheaders.com — даёт оценку и детальный разбор каждого заголовка
- Mozilla Observatory (observatory.mozilla.org) — более детальный анализ с рекомендациями
- curl в терминале — быстрая проверка конкретных заголовков
- Google Lighthouse — проверяет в том числе некоторые аспекты безопасности
Через curl это выглядит так:
curl -I https://example.com
# Или только заголовки безопасности:
curl -I https://example.com 2>/dev/null | grep -i -E "(strict-transport|x-frame|x-content|content-security|referrer|permissions)"
По опыту, после первой настройки всегда что-то идёт не так. Либо CSP блокирует нужный скрипт аналитики, либо X-Frame-Options ломает встроенный виджет. Поэтому я всегда настраиваю сначала на тестовом окружении, проверяю все основные сценарии использования сайта, и только потом выкатываю на продакшн.
Кстати, если у вас ещё нет нормального мониторинга сайта в целом — это отдельная большая тема. Рекомендую почитать Как настроить мониторинг сайта: полное руководство. Заголовки безопасности — часть общей картины.
Типичные ошибки при настройке
За годы практики я видел одни и те же ошибки снова и снова. Вот самые распространённые:
1. Заголовки выставляются не для всех ответов. Классическая ошибка в nginx — забыть слово always в add_header. Без него заголовок добавляется только к ответам с кодом 2xx и 3xx. А 404 и 500 — без заголовков. Злоумышленники это используют.
2. Конфликт заголовков из нескольких мест. Если заголовки выставляются и в nginx, и в PHP, и в .htaccess — могут быть дубли или конфликты. Я видел сайты, где Content-Security-Policy приходил дважды с разными значениями. Браузер в таком случае берёт более строгое, что может сломать функциональность.
3. Слишком агрессивный CSP сразу. Поставить default-src 'none' без предварительного анализа — это гарантированно сломать сайт. Всегда начинайте с режима Report-Only.
4. Игнорирование поддоменов. Выставили HSTS с includeSubDomains, а какой-то технический поддомен работает по HTTP? Всё, он стал недоступен. Перед включением includeSubDomains проверьте все поддомены.
5. Не обновляют заголовки при добавлении новых сервисов. Подключили новый виджет обратного звонка, новую систему аналитики — и CSP начинает их блокировать. Нужно обновлять политику. Это не разовая настройка, а процесс.
По теме безопасности в целом — если ещё не читали, очень рекомендую статью Настройка Firewall для защиты сайта: полное руководство 2026. Заголовки безопасности — это один слой защиты, но не единственный.
Приоритеты: с чего начать прямо сейчас
Если вы читаете это и у вас ещё ничего не настроено — не пытайтесь сразу сделать всё идеально. Вот мой список по приоритетам:
- Сначала HTTPS — если у вас ещё нет SSL, всё остальное бессмысленно. Почитайте Что такое SSL-сертификат и зачем он нужен сайту.
- HSTS — выставьте с небольшим max-age, постепенно увеличивайте
- X-Content-Type-Options: nosniff — одна строка, ноль побочных эффектов
- X-Frame-Options: SAMEORIGIN — тоже просто, если нет внешних iframe
- Referrer-Policy — выставьте strict-origin-when-cross-origin
- Permissions-Policy — закройте то, что не используете
- CSP — запустите в режиме Report-Only, соберите данные, постройте политику
Если нужна помощь с настройкой — я занимаюсь этим в рамках доработки сайта. Обычно полная настройка заголовков безопасности с аудитом занимает 2-4 часа работы, в зависимости от сложности проекта. Это небольшие инвестиции, которые реально снижают риски взлома и улучшают оценки в аудитах безопасности.
Заголовки безопасности — это не rocket science. Это базовая гигиена, которую должен иметь каждый сайт в 2026 году. Браузеры их поддерживают, инструменты для проверки есть бесплатные, конфиги я дал выше. Осталось только сделать.
Хотите защитить свой сайт с помощью правильных HTTP-заголовков?
Наши специалисты настроят все необходимые заголовки безопасности и обеспечат надёжную защиту вашего веб-проекта.