Выберите продукт

Гигиена заголовков в обратном прокси: X‑Forwarded‑*, hop‑by‑hop и доверенные IP

Если nginx стоит обратным прокси, от чистоты заголовков зависят безопасность, корректные IP в логах и редиректы. Разбираем практику: как вычистить hop‑by‑hop, безопасно формировать X‑Forwarded‑*, настроить trusted IP/real_ip и proxy_set_header.
Гигиена заголовков в обратном прокси: X‑Forwarded‑*, hop‑by‑hop и доверенные IP

Обратный прокси давно перестал быть просто «пробросом» трафика. Заголовки 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-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE (кроме значения trailers)
  • Trailer
  • Transfer-Encoding
  • Upgrade (особый случай — вебсокеты)

Эти заголовки не должны «утекать» к вашему приложению. Большинство 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 проходит далее.

Диаграмма обработки real_ip и доверенных прокси в nginx

X‑Forwarded‑* и Forwarded (RFC 7239)

Семейство X-Forwarded-* де‑факто стандарт в мире прокси:

  • X-Forwarded-For — цепочка IP от клиента к каждому прокси
  • X-Forwarded-Proto — исходная схема (http или https)
  • X-Forwarded-Host — исходный Host
  • X-Forwarded-Port — исходный порт

Есть и стандартизованный Forwarded из RFC 7239: он объединяет сведения в одном заголовке (for=, proto=, host=, by=). На практике приложения чаще ждут именно X-Forwarded-*, поэтому разумно формировать оба, но приоритет отдать устойчивым к спуфингу X-Forwarded-*.

Ключевой принцип: никогда не доверяйте заголовкам X-Forwarded-*, пришедшим от клиента. Их должна формировать только ваша граничная точка — обратный прокси. Исключение — когда непосредственный пир уже известен и доверен (LB, CDN), и вы настроили доверенные IP.

Если вы настраиваете HTTPS и строгие редиректы, пригодится материал Миграция домена: 301, HSTS и SSL.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой 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-сертификаты.

Поток PROXY protocol между балансировщиком и nginx

Шаблоны безопасных заголовков для 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";
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Системные настройки: работа с «неправильными» заголовками

Полезные директивы 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 это дает прозрачные логи, предсказуемое поведение приложений и меньшую поверхность для атак. Один раз настроили — и возвращайтесь к теме лишь при изменении топологии или диапазонов доверенных адресов.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND

Если локальный DNS в Debian или Ubuntu не стартует с ошибкой address already in use, причина часто в systemd-resolved и DNSStubLis ...
Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting

Ошибка mount.nfs: access denied by server while mounting в Debian и Ubuntu обычно указывает на проблему на стороне NFS-сервера: не ...
Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home

Если в Debian или Ubuntu DNS-сервер не стартует из-за ошибки port 53 busy, часто причина в systemd-resolved с локальным слушателем ...