Обратный прокси давно перестал быть просто «пробросом» трафика. Заголовки HTTP — это часть вашей поверхности атаки и источник критичных метаданных для приложений: схема запроса, реальный IP клиента, порт, исходный хост. Неправильная гигиена заголовков в reverse proxy на nginx ведет к спуфингу IP в логах, обходу ACL по IP, поломке редиректов, неверной генерации абсолютных ссылок и проблемам с безопасностью.
Зачем вообще заниматься «гигиеной» заголовков
Часть заголовков являются hop‑by‑hop — они предназначены только для текущего участка соединения и не должны проходить дальше по цепочке. Другие — end‑to‑end — должны безопасно сохраняться и добавляться к цепочке. К сожалению, браузер или злоумышленник может отправить «легитимные» на вид заголовки, например X-Forwarded-For или X-Forwarded-Proto, и если ваш прокси их вслепую ретранслирует, приложение «поверит» ложной информации. Задача гигиены — очистить входящие заголовки, строго сформировать исходящие и корректно доверять только проверенным источникам.
Hop‑by‑hop против end‑to‑end
По спецификации HTTP заголовки hop‑by‑hop относятся только к одному участку (клиент ↔ прокси или прокси ↔ upstream). Их нельзя пересылать дальше. К ним относятся:
Connectionи перечисленные в нем расширенияKeep-AliveProxy-AuthenticateProxy-AuthorizationTE(кроме значенияtrailers)TrailerTransfer-EncodingUpgrade(особый случай — вебсокеты)
Эти заголовки не должны «утекать» к вашему приложению. Большинство reverse proxy, включая nginx, по умолчанию не проксируют часть hop‑by‑hop заголовков, но хорошо иметь явный и проверяемый конфиг.
Практика: явная санитария hop‑by‑hop в nginx
Безопасная привычка — обнулять явно то, что точно не должно уходить к upstream. Для обычного HTTP‑трафика:
location / {
# Удаляем hop-by-hop
proxy_set_header Connection "";
proxy_set_header Keep-Alive "";
proxy_set_header Proxy-Authenticate "";
proxy_set_header Proxy-Authorization "";
proxy_set_header TE "";
proxy_set_header Trailer "";
proxy_set_header Transfer-Encoding "";
proxy_set_header Upgrade ""; # для обычных запросов не нужен
proxy_pass http://app;
}
Для вебсокетов, наоборот, Upgrade нужен, но только если вы сами инициируете апгрейд на основании входящего запроса:
map $http_upgrade $upgrade_needed {
default upgrade;
"" "";
}
location /ws/ {
proxy_set_header Connection $upgrade_needed;
proxy_set_header Upgrade $http_upgrade;
proxy_pass http://ws_backend;
}
Так вы контролируете, когда и как заголовок Upgrade проходит далее.

X‑Forwarded‑* и Forwarded (RFC 7239)
Семейство X-Forwarded-* де‑факто стандарт в мире прокси:
X-Forwarded-For— цепочка IP от клиента к каждому проксиX-Forwarded-Proto— исходная схема (httpилиhttps)X-Forwarded-Host— исходный HostX-Forwarded-Port— исходный порт
Есть и стандартизованный Forwarded из RFC 7239: он объединяет сведения в одном заголовке (for=, proto=, host=, by=). На практике приложения чаще ждут именно X-Forwarded-*, поэтому разумно формировать оба, но приоритет отдать устойчивым к спуфингу X-Forwarded-*.
Ключевой принцип: никогда не доверяйте заголовкам
X-Forwarded-*, пришедшим от клиента. Их должна формировать только ваша граничная точка — обратный прокси. Исключение — когда непосредственный пир уже известен и доверен (LB, CDN), и вы настроили доверенные IP.
Если вы настраиваете HTTPS и строгие редиректы, пригодится материал Миграция домена: 301, HSTS и SSL.
Доверенные IP и модуль real_ip в nginx
Чтобы извлекать реальный IP клиента при наличии промежуточных прокси, в nginx есть модуль real_ip. Он позволяет:
- Задать источники, от которых можно принимать «оригинальный IP» (
set_real_ip_from) - Указать, из какого места его брать (
real_ip_headerилиproxy_protocol) - Глубоко разбирать цепочку при нескольких прокси (
real_ip_recursive on)
Базовый вариант: один обратный прокси на границе
Если nginx — это единственный граничный прокси, безопаснее всего игнорировать клиентские X-Forwarded-* и сформировать свои:
server {
# Никому не доверяем, real_ip не включаем
location / {
# Сами формируем end-to-end заголовки
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Host $host;
# Санитария hop-by-hop, как выше
proxy_set_header Connection "";
proxy_set_header Keep-Alive "";
proxy_set_header Proxy-Authenticate "";
proxy_set_header Proxy-Authorization "";
proxy_set_header TE "";
proxy_set_header Trailer "";
proxy_set_header Transfer-Encoding "";
proxy_set_header Upgrade "";
proxy_pass http://app;
}
}
Так вы гарантированно не пропустите клиентский спуф.
Цепочка: CDN/LB → ваш nginx → приложение
Когда перед nginx стоит балансировщик или CDN, он обычно добавляет свои X-Forwarded-*. Нам важно либо полностью доверять этому пиру, либо игнорировать его заголовки. Безопасный подход — доверять только IP‑сетям CDN/LB и включить разбор цепочки:
http {
# Доверенные адреса (пример)
set_real_ip_from 203.0.113.0/24; # ваш LB
set_real_ip_from 198.51.100.0/24; # IP диапазон CDN
real_ip_header X-Forwarded-For;
real_ip_recursive on;
server {
location / {
# Если пир доверенный, $remote_addr уже равен реальному клиентскому IP
# Формируем XFF так, чтобы сохранять цепочку только от доверенных узлов
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Host $host;
proxy_pass http://app;
}
}
}
$proxy_add_x_forwarded_for добавит текущий $remote_addr к существующей цепочке. Благодаря real_ip_recursive $remote_addr уже «очищен» и соответствует реальному клиенту. Важно: указанными в set_real_ip_from должны быть только ваши контролируемые источники. Не добавляйте туда «весь интернет».
Сохранить цепочку, не доверяя клиенту: map по пир‑IP
Если вы хотите сохранять XFF, когда запрос пришел от доверенного балансировщика, но полностью срезать клиентские заголовки в остальных случаях, используйте geo и map:
geo $from_trusted_proxy {
default 0;
203.0.113.0/24 1;
198.51.100.0/24 1;
}
map $from_trusted_proxy $xff_sane {
1 "$http_x_forwarded_for, $remote_addr";
0 "$remote_addr";
}
server {
location / {
proxy_set_header X-Forwarded-For $xff_sane;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Host $host;
proxy_pass http://app;
}
}
Так вы сохраняете цепочку только если пир доверен, исключая спуф от клиента.
PROXY protocol: когда заголовков недостаточно
Самый надежный способ передать исходный адрес клиента от балансировщика — PROXY protocol v1/v2. Он идет перед HTTP и не зависит от заголовков. В nginx:
server {
listen 443 ssl proxy_protocol;
set_real_ip_from 203.0.113.10; # адрес LB
real_ip_header proxy_protocol;
location / {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_pass http://app;
}
}
Если балансировщик поддерживает PROXY protocol, это устраняет риск спуфинга клиентом X-Forwarded-For и упрощает конфиг. Для терминации TLS используйте актуальные SSL-сертификаты.

Шаблоны безопасных заголовков для upstream
Соберем минимальный и предсказуемый набор заголовков, которые nginx будет отправлять приложению:
# Базовый профиль (HTTP)
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $remote_addr; # либо $proxy_add_x_forwarded_for при trust chain
# Удаление hop-by-hop
proxy_set_header Connection "";
proxy_set_header Keep-Alive "";
proxy_set_header Proxy-Authenticate "";
proxy_set_header Proxy-Authorization "";
proxy_set_header TE "";
proxy_set_header Trailer "";
proxy_set_header Transfer-Encoding "";
proxy_set_header Upgrade "";
Если хотите формировать также Forwarded для совместимости, используйте простой и безопасный вариант. Формирование полностью валидного RFC 7239 со скобками и кавычками не всегда тривиально, поэтому придерживайтесь консервативного формата:
proxy_set_header Forwarded "for=$remote_addr;proto=$scheme;host=$host";
Системные настройки: работа с «неправильными» заголовками
Полезные директивы nginx, влияющие на обработку заголовков:
ignore_invalid_headers on;— по умолчанию включено; отклоняет заголовки с недопустимыми символамиunderscores_in_headers off;— по умолчанию выключено; если у вас upstream шлет заголовки с подчеркиваниями, включайте осознанноlarge_client_header_buffers— лимиты на размер и число буферов для заголовков; помогает избежать DoS за счет слишком длинных заголовков
Всегда измеряйте и устанавливайте лимиты, исходя из ваших реальных заголовков (OAuth‑токены, cookie, трассировка), не забывая про запас.
Логи и приватность: как логировать реальный клиентский IP
После настройки real_ip убедитесь, что access_log пишет корректный адрес. В формате логов используйте $remote_addr — он уже учитывает ваши правила trust chain. Если хотите видеть исходную цепочку XFF, логируйте отдельным полем, но помните о приватности: там могут быть внутренние адреса.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
Так вы сможете сопоставлять инциденты и понимать, откуда именно пришел трафик.
Проверка и отладка
Простейший тест спуфинга: попытайтесь отправить поддельный X-Forwarded-For и убедитесь, что приложение получает только реальный IP или корректно построенную цепочку.
# Пример: отправляем поддельный XFF
curl -H "X-Forwarded-For: 1.2.3.4" -H "X-Forwarded-Proto: http" -I https://example.com/
# Проверка заголовков, которые дошли до upstream (если приложение их отражает)
Для сложных случаев помогайте себе временным логированием всех входящих заголовков на уровне приложения или включайте детальные логи nginx о заголовках запросов и ответов в изолированном окружении.
Частые ошибки и антипаттерны
- Доверять всем заголовкам X‑Forwarded‑*. Клиент может подделать их в один клик. Формируйте их сами на границе.
- Добавлять
0.0.0.0/0вset_real_ip_from. Это превращает любой клиентский XFF в «истину» и ломает безопасность. - Смешивать
$proxy_add_x_forwarded_forбез real_ip. Вы сохраните и легитимную цепочку, и вредоносную часть от клиента — худший вариант. - Забывать про hop‑by‑hop. «Течет»
Connection: keep-alive,Transfer-EncodingилиTE— приложение может вести себя непредсказуемо. - Переиспользовать правила для вебсокетов в обычных локациях.
UpgradeиConnection: upgradeдолжны проходить только там, где это действительно вебсокеты. - Игнорировать лимиты заголовков. Без настройки
large_client_header_buffersи ограничений вы рискуете получить DoS длинными заголовками.
Чек‑лист гигиены заголовков для обратного прокси
- Определите граничные точки доверия и список действительно доверенных IP
- Включите
real_ipсset_real_ip_fromиreal_ip_headerили PROXY protocol - Всегда формируйте
X-Forwarded-*на границе; не доверяйте клиентским значениям - Для цепочек прокси используйте
real_ip_recursiveи/илиmapна основе доверенного пир‑IP - Явно обнуляйте hop‑by‑hop заголовки для обычных HTTP‑локаций
- Настройте отдельную локацию/сервер для вебсокетов с
Upgrade - Проверьте
ignore_invalid_headers,underscores_in_headers, лимиты буферов - Сформируйте безопасный
log_formatи проверьте, что в логах верный клиентский IP - Добавьте тесты спуфинга в чек‑листы развертывания
Итоги
Гигиена заголовков — это набор обязательных практик: не доверяйте клиентским X-Forwarded-*, формируйте свои, используйте real_ip и доверенные IP, обнуляйте hop‑by‑hop и включайте PROXY protocol там, где это возможно. На собственном сервере или VDS это дает прозрачные логи, предсказуемое поведение приложений и меньшую поверхность для атак. Один раз настроили — и возвращайтесь к теме лишь при изменении топологии или диапазонов доверенных адресов.


