HTTP-заголовки в .htaccess

Справочник HTTP-заголовков, которые удобно выставлять через .htaccess модулем mod_headers (директива Header, для заголовков запроса — RequestHeader): что делает каждый, как написать, грабли.

Собрать набор заголовков безопасности чекбоксами — генератор (блок «Security headers»); готовый блок — рецепт «Заголовки безопасности»; общая справка по Header/RequestHeader как директивам — справочник директив. Важно: заголовки, которые сервер/PHP/CMS уже выставляет, лучше менять с Header always set … (перезапишет, в т. ч. для ответов об ошибках), либо не дублировать; и оборачивать блок в <IfModule mod_headers.c>…</IfModule>, чтобы .htaccess не падал на хостинге без модуля.

Как ставить заголовки в .htaccess

Посмотреть, какие заголовки реально отдаёт ваш сайт — чекер сервера.

mod_headers даёт две директивы — Header (заголовок ответа) и RequestHeader (заголовок запроса, увидит бэкенд/прокси).

  • Header — добавить/изменить/удалить заголовок ответа. Действия: set (установить, заменив — для ответа об ошибке может не сработать), always set (то же, но для всех ответов, включая ошибки и редиректы — для заголовков безопасности почти всегда нужно always), append (добавить значение через запятую), add (добавить ещё одну строку заголовка — для multi-value вроде Set-Cookie/Link), unset (удалить), edit ИМЯ <regex> <замена> / edit* (regex-замена в значении), merge (как append, но без дублей). Опц. условие env=ИМЯ / env=!ИМЯ. Синтаксис: Header [always] <действие> <Имя> ["значение"] [env=…]. Пример: Header always set X-Frame-Options "SAMEORIGIN". (Грабли: если PHP/CMS сама шлёт заголовок — Header always set … перезапишет; для «жёсткой» замены — сначала Header unset Имя, потом Header set Имя …. Заголовки можно ставить адресно — внутри <FilesMatch> / <If>.)
  • RequestHeader — то же для заголовка запроса (который увидит бэкенд / прокси / CGI, не клиент). Синтаксис: RequestHeader <действие> <Имя> ["значение"]. Пример: RequestHeader set X-Forwarded-Proto "https".
  • <IfModule mod_headers.c> — оборачивайте блок с Header-директивами, чтобы .htaccess не вернул 500 на сервере без mod_headers. Синтаксис: <IfModule mod_headers.c> Header always set X-Content-Type-Options "nosniff" </IfModule>. Пример: то же.

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

Базовая «оборона» сайта на уровне ответа — кликджекинг, MIME-sniffing, утечка реферера, разрешения фич, изоляция. Все — с Header always set …. Собрать нужные чекбоксами — генератор (блок «Security headers»), готовый блок — рецепт.

  • Strict-Transport-Security (HSTS) — браузер запоминает, что сайт надо открывать только по HTTPS, в течение max-age секунд. Синтаксис: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains". Пример: то же; с ; preload — заявка на встроенный в браузеры preload-список. (Грабли: после включения сайт нельзя открыть по http:// в течение max-age, даже если TLS сломается; includeSubDomains распространяет это на все поддомены; preload практически необратим (заявка на удаление в hstspreload.org); включать ТОЛЬКО когда сайт навсегда на HTTPS, начинать с маленького max-age, например 300.)
  • Content-Security-Policy (CSP) — белый список источников для скриптов, стилей, картинок, фреймов, XHR — главная защита от XSS и встраивания. Синтаксис: Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; …". Пример: Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'". (Грабли: легко сломать inline-<script>/<style>, внешние CDN, аналитику; начинайте с Content-Security-Policy-Report-Only + report-uri/report-to, смотрите нарушения, потом включайте боевой; 'unsafe-inline'/'unsafe-eval' ослабляют защиту; frame-ancestors заменяет X-Frame-Options.)
  • Content-Security-Policy-Report-Only — то же, что CSP, но ничего не блокирует — только шлёт отчёты о том, что нарушило бы политику. Синтаксис: Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report". Пример: то же. (Используйте, чтобы обкатать политику без поломок на проде.)
  • X-Frame-Options — можно ли встраивать сайт в <iframe>: DENY (никому) / SAMEORIGIN (только со своего домена). Защита от кликджекинга. Синтаксис: Header always set X-Frame-Options "SAMEORIGIN". Пример: то же. (Для современных браузеров покрывается CSP frame-ancestors, но X-Frame-Options ещё нужен для совместимости; ALLOW-FROM большинством браузеров не поддерживается — используйте CSP.)
  • X-Content-Type-Optionsnosniff — запретить браузеру «угадывать» MIME-тип по содержимому. Синтаксис: Header always set X-Content-Type-Options "nosniff". Пример: то же.
  • Referrer-Policy — сколько информации из адреса страницы утекает в Referer: no-referrer, same-origin, strict-origin-when-cross-origin (рекомендуемый дефолт), origin, unsafe-url (не надо). Синтаксис: Header always set Referrer-Policy "strict-origin-when-cross-origin". Пример: то же.
  • Permissions-Policy (бывш. Feature-Policy) — какие браузерные фичи разрешены и кому: geolocation=(), camera=(self), microphone=(), fullscreen=(self), payment=(), interest-cohort=(). Синтаксис: Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()". Пример: то же. (У старого Feature-Policy другой синтаксис значений — ставьте новое имя.)
  • Cross-Origin-Opener-Policy (COOP) — изолировать окно браузера от окон других origin: same-origin / same-origin-allow-popups / unsafe-none. Синтаксис: Header always set Cross-Origin-Opener-Policy "same-origin". Пример: то же. (same-origin может сломать межоконное взаимодействие с виджетами/OAuth-попапами; для попапов — same-origin-allow-popups.)
  • Cross-Origin-Embedder-Policy (COEP) — требовать, чтобы все встроенные кросс-origin-ресурсы явно разрешали встраивание: require-corp / credentialless / unsafe-none. Синтаксис: Header always set Cross-Origin-Embedder-Policy "require-corp". Пример: то же. (Грабли: require-corp ломает встраивание любого стороннего ресурса без Cross-Origin-Resource-Policy/Access-Control-* — включать осознанно.)
  • Cross-Origin-Resource-Policy (CORP) — кто может встраивать ваш ресурс: same-origin / same-site / cross-origin. Синтаксис: Header set Cross-Origin-Resource-Policy "same-origin". Пример: то же. (Для публичных картинок/шрифтов, которые должны грузиться откуда угодно, — cross-origin.)
  • X-XSS-Protection — рекомендуемое значение 0 (отключить старую встроенную фильтрацию XSS — она убрана/небезопасна; защита — CSP). Синтаксис: Header always set X-XSS-Protection "0". Пример: то же. (Значения 1 / 1; mode=block устарели и могли создавать уязвимости — ставьте 0 или не ставьте вовсе.)
  • X-Permitted-Cross-Domain-Policiesnone — запретить Adobe Flash/PDF-плагинам кросс-доменный доступ через crossdomain.xml. Синтаксис: Header always set X-Permitted-Cross-Domain-Policies "none". Пример: то же. (Сейчас почти неактуально — Flash мёртв; ставят «для полноты» в security-аудитах.)

Заголовки безопасности — лишь часть картины: см. также гид «Безопасность сайта через .htaccess» — защита файлов, доступ по IP, пароль, запрет выполнения PHP в загрузках, анти-бот.

CORS (доступ с других доменов)

Когда JS на странице a.com должен обратиться к ресурсу на b.com — браузер пускает это только если b.com прислал нужные Access-Control-*-заголовки.

  • Access-Control-Allow-Origin — какому домену (origin) разрешён cross-origin-доступ: * (всем) или конкретный https://app.example.com. Синтаксис: Header set Access-Control-Allow-Origin "*". Пример: то же / Header set Access-Control-Allow-Origin "https://app.example.com". (Грабли: * несовместим с Access-Control-Allow-Credentials: true (браузер отбросит); несколько доменов через запятую нельзя — нужно SetEnvIf Origin "^https://(a|b)\.example\.com$" CORS_ORIGIN=$0 + Header set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN + Header append Vary Origin.)
  • Access-Control-Allow-Methods — какие HTTP-методы разрешены в cross-origin-запросе (отдаётся в ответ на preflight OPTIONS). Синтаксис: Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS". Пример: то же.
  • Access-Control-Allow-Headers — какие нестандартные заголовки запроса разрешены. Синтаксис: Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With". Пример: то же.
  • Access-Control-Allow-Credentialstrue — разрешить отправку кук/HTTP-авторизации в cross-origin-запросах (тогда Allow-Origin обязан быть конкретным, не *). Синтаксис: Header set Access-Control-Allow-Credentials "true". Пример: то же.
  • Access-Control-Max-Age — сколько секунд браузер может кэшировать ответ на preflight, чтобы не слать OPTIONS перед каждым запросом. Синтаксис: Header set Access-Control-Max-Age "86400". Пример: то же.
  • Access-Control-Expose-Headers — какие заголовки ответа JS-код вправе прочитать (по умолчанию доступен ограниченный набор). Синтаксис: Header set Access-Control-Expose-Headers "Content-Length, X-Total-Count". Пример: то же.
  • Vary: Origin — при динамическом Access-Control-Allow-Origin (зависящем от Origin запроса) обязательно добавляйте Header append Vary Origin — иначе прокси/CDN отдаст закэшированный Allow-Origin для другого домена. Синтаксис: Header append Vary Origin. Пример: то же.

Preflight-запрос OPTIONS Apache обычно отдаёт как обычный (404/405 + тело). Если нужен «голый» preflight-ответ — пример: RewriteEngine On + RewriteCond %{REQUEST_METHOD} OPTIONS + RewriteRule ^ - [R=200,L] (вернёт 200 без тела; сами CORS-заголовки добавит блок Header set Access-Control-* …). Подробнее про правила — тестер RewriteRule, рецепты.

Кэширование

Заголовки, которыми вы говорите браузеру/прокси/CDN, сколько хранить ответ и когда перепроверять. Часто проще задать через mod_expires (ExpiresByType) — см. директивы mod_expires; ниже — сами заголовки и их смысл.

  • Cache-Control — главный заголовок кэша: max-age=N (сколько секунд ответ свеж), public/private (можно ли кэшировать в общих прокси/CDN или только в браузере), no-cache (кэшировать можно, но перед использованием ревалидировать на сервере), no-store (вообще не кэшировать — для приватных данных), immutable (в течение max-age не ревалидировать даже при перезагрузке), s-maxage=N (max-age только для общих кэшей/CDN), must-revalidate. Синтаксис: Header set Cache-Control "max-age=31536000, public, immutable". Пример (статика, внутри <FilesMatch "\.(css|js|woff2|jpe?g|png|webp|svg|ico)$">): то же; (HTML): Header set Cache-Control "no-cache". (Грабли: статику кэшируйте надолго ТОЛЬКО при версионировании имён (style.v2.css) — иначе пользователи получат старую; HTML — no-cache или короткий max-age; не путайте no-cache (ревалидировать) и no-store (не хранить вообще).)
  • Expires — абсолютная дата/время, до которой ответ свеж (Expires: Thu, 31 Dec 2026 23:59:59 GMT) — старый аналог Cache-Control: max-age. Если есть оба — Cache-Control главнее. Синтаксис: Header set Expires "Wed, 01 Jan 2025 00:00:00 GMT". Пример: то же. (Руками задавать абсолютную дату неудобно — обычно проще mod_expires: ExpiresActive On + ExpiresByType … "access plus 1 year" — это автоматически считает и Expires, и Cache-Control: max-age.)
  • ETag — «отпечаток» содержимого ответа; браузер при повторном запросе шлёт If-None-Match: <etag>, сервер отвечает 304 Not Modified (без тела), если не изменилось. Apache генерирует сам (см. FileETag в справочнике директив). Синтаксис: Header unset ETag (часто отключают). Пример: Header unset ETag + FileETag None. (Грабли: дефолтный ETag в Apache включает inode файла → на кластере из нескольких серверов ETag-и разъезжаются и кэш не срабатывает; либо FileETag MTime Size (без inode), либо отключить и полагаться на Cache-Control/Last-Modified.)
  • Last-Modified — дата последнего изменения файла; браузер шлёт If-Modified-Since, сервер отвечает 304, если файл не менялся. Apache ставит сам для статических файлов. Синтаксис: Header unset Last-Modified (если зачем-то нужно убрать). Пример: обычно ничего не делают — Apache сам.
  • Vary — от каких заголовков запроса зависит ответ — чтобы кэши хранили разные варианты: Vary: Accept-Encoding (есть сжатая и несжатая версии), Vary: Origin (CORS), Vary: Accept (контент-негоциация). Синтаксис: Header append Vary Accept-Encoding. Пример: то же. (Грабли: Vary: User-Agent или Vary: Cookie почти полностью убивает эффективность общего кэша — добавляет вариант на каждый UA/куку; используйте только при реальной необходимости.)
  • Pragmano-cache — реликт HTTP/1.0; современные браузеры ориентируются на Cache-Control. Иногда ставят «на всякий» вместе с Cache-Control: no-cache. Синтаксис: Header set Pragma "no-cache". Пример: то же. (Только Pragma без Cache-Control ненадёжно; современным клиентам не нужен.)

Подробнее про директивы кэширования — рецепты, mod_expires.

Практическое применение этих заголовков (что кэшировать на год, что — на ноль секунд, как не застрять на старой версии) — в гиде «Ускорение сайта через .htaccess».

Контент, индексация, разное

Заголовки про индексацию поисковиками, скачивание файлов, ресурс-хинты и кодирование.

  • X-Robots-Tag — управление индексацией заголовком (а не мета-тегом) — полезно для не-HTML: PDF, картинки, файлы, ответы API. Значения: noindex, nofollow, none (=noindex, nofollow), noarchive, nosnippet; адресно для бота: googlebot: noindex. Синтаксис: Header set X-Robots-Tag "noindex, nofollow". Пример (внутри <FilesMatch "\.(pdf|docx?)$">): Header set X-Robots-Tag "noindex".
  • Content-Disposition — как браузер обращается с ответом: attachment; filename="report.pdf" — скачать как файл (с предложенным именем); inline — показать в окне браузера. Синтаксис: Header set Content-Disposition "attachment; filename=\"file.zip\"". Пример (внутри <FilesMatch "\.(zip|tar\.gz)$">): Header set Content-Disposition attachment. (Для нелатинских имён файлов нужна форма filename*=UTF-8''… — это лучше делать в коде приложения, не в .htaccess.)
  • Content-Type — MIME-тип тела ответа (text/html; charset=UTF-8). Обычно задаётся не Header-ом, а AddType (по расширению) или ForceType (всем файлам в каталоге) из mod_mime — см. директивы mod_mime. Header set Content-Type … тоже работает (внутри <Files>), но AddType чище. Синтаксис: AddType application/manifest+json .webmanifest (предпочтительно). Пример: то же.
  • Linkrel-связи и ресурс-хинты заголовком (а не <link> в HTML): </style.css>; rel=preload; as=style, <https://fonts.gstatic.com>; rel=preconnect, <https://example.com/page>; rel=canonical. Синтаксис: Header add Link "</css/main.css>; rel=preload; as=style". Пример: то же. (Используйте Header add (не set), если Link-заголовков несколько.)
  • Content-Encoding — каким кодированием сжато тело (gzip, br). Обычно его выставляет mod_deflate/mod_brotli сам при сжатии — руками не нужно. Исключение: на диске лежит уже сжатый файл (app.js.gz), который вы отдаёте как .js — тогда Header set Content-Encoding gzip (чище — AddEncoding gzip .gz, см. фильтры/кодировки). Синтаксис: Header set Content-Encoding gzip (внутри <FilesMatch "\.gz$">). Пример: то же. (Грабли: руками поставленный Content-Encoding без реального сжатия = битый, нечитаемый ответ.)
  • Accept-Ranges — поддержка частичных запросов (докачка, перемотка видео): bytes (поддерживается) / none (нет). Apache для статики ставит bytes сам. Синтаксис: Header set Accept-Ranges none (отключить — редко нужно). Пример: то же.
  • X-DNS-Prefetch-Controlon / off — разрешить браузеру заранее резолвить DNS доменов из ссылок на странице. Синтаксис: Header set X-DNS-Prefetch-Control "on". Пример: то же.
  • Clear-Site-Data — попросить браузер очистить данные сайта: "cache", "cookies", "storage", "*". Ставят обычно на конкретный URL (например /logout). Синтаксис: Header set Clear-Site-Data "\"cookies\", \"storage\"". Пример (внутри <Files "logout.php">): то же. (Значения — JSON-строки в двойных кавычках внутри значения заголовка.)

Заголовки запроса (RequestHeader)

RequestHeader меняет то, что увидит бэкенд/прокси/CGI, а не клиент. В значениях Header/RequestHeader доступны %{VARNAME}e (переменная окружения), %{VARNAME}s (SSL-переменная при mod_ssl) и спецтокены (%t, %D); синтаксиса %{HTTP:Имя} тут нет (это mod_rewrite/mod_setenvif/expr) — чтобы прокинуть входящий заголовок, его сначала кладут в env-переменную (SetEnvIf …), потом читают как %{…}e.

  • X-Forwarded-Proto — сказать бэкенду/приложению, по какому протоколу пришёл запрос на фронт (https/http) — нужно за reverse-proxy/балансировщиком, который терминирует TLS, чтобы приложение генерировало https://-ссылки. Синтаксис: RequestHeader set X-Forwarded-Proto "https". Пример: то же (если этот Apache сам стоит за прокси и снаружи HTTPS).
  • X-Forwarded-For — передать/добавить IP клиента вниз по цепочке проксей. Обычно это делает прокси сам, но иногда нужно явно. Синтаксис: RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}e". Пример: то же. (Доверяйте X-Forwarded-For только от своих проксей — клиент может его подделать.)
  • Authorization — иногда заголовок Authorization не доходит до PHP-FPM/CGI (теряется по дороге) — частая боль REST API за FPM. Решения: CGIPassAuth On (Apache 2.4.13+, проще всего — пробрасывает Authorization в окружение скрипта) либо SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1 (тогда PHP читает $_SERVER['HTTP_AUTHORIZATION']; при желании добавить RequestHeader set Authorization "%{HTTP_AUTHORIZATION}e"). Синтаксис: CGIPassAuth On или SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1. Пример: то же.

Генератор .htaccess (блок «Security headers») · Рецепты .htaccess (рецепт «Заголовки безопасности») · Справочник директив (Header / RequestHeader, mod_expires, mod_mime) · Проверка .htaccess (линтер) · Тестер RewriteRule · Как работает mod_rewrite · Блокировка по referer · Фильтрация по IP