Модель доверия прокси-заголовков: realip и защита от спуфинга

Рейтинг: 79.6% · 15 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Модель доверия прокси-заголовков: realip и защита от спуфинга

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга (вы здесь)
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Боль: за прокси Nginx видит не клиента, а соседа

Ты поднял балансировщик, поставил перед ним Cloudflare, добавил второй слой nginx внутри кластера - и вдруг в логах у тебя один и тот же IP на всех запросах. Rate limit срабатывает не на абьюзера, а на всю аудиторию сразу. Geo-блокировка пропускает кого попало. Авторизация по IP пускает чужих. Аналитика показывает, что 100% трафика идёт из одной точки. Знакомо?

Причина простая и фундаментальная. Переменная $remote_addr в nginx - это адрес того, кто открыл TCP-соединение к этому конкретному nginx. Если перед ним стоит балансировщик, CDN или ещё один прокси - то $remote_addr это адрес ближайшего прокси, а вовсе не клиента. Клиентский адрес едет внутри HTTP-заголовка (обычно X-Forwarded-For), но nginx по умолчанию на него вообще не смотрит. Для него это просто строка в запросе.

Поэтому этот урок идёт до уроков про rate limit, логи и авторизацию. Все они опираются на $binary_remote_addr и $remote_addr. Если ты не починил клиентский IP в самом начале обработки запроса, то строишь rate limit на лжи, считаешь географию по адресу дата-центра CDN и пишешь в лог бесполезные данные. Сначала восстанавливаем правду об источнике, потом всё остальное. Тема nginx real ip - это не косметика логов, это фундамент.

Изображение

Механика модуля realip: как nginx подменяет $remote_addr

За восстановление клиентского адреса отвечает модуль ngx_http_realip_module. Он встроен в стандартную сборку, но включается флагом --with-http_realip_module - в официальных пакетах nginx, Angie и OpenResty он уже есть, проверять не нужно. Логика у него ровно три директивы.

Код: Выделить всё

set_real_ip_from   10.0.0.0/8;        # кому мы ДОВЕРЯЕМ
real_ip_header     X-Forwarded-For;   # откуда брать настоящий адрес
real_ip_recursive  on;                # разворачивать цепочку прокси
Работает это так. Приходит запрос. Nginx смотрит на фактический адрес соединения - на тот самый $remote_addr. Если этот адрес попадает в одну из сетей, перечисленных в set_real_ip_from, модуль считает соединение доверенным и достаёт адрес из заголовка, указанного в real_ip_header. Этим адресом он перезаписывает $remote_addr и $binary_remote_addr. Всё. Дальше по цепочке обработки - логи, limit_req, geo, map, твоя авторизация - видят уже клиентский адрес, как будто клиент пришёл напрямую.

Критичный момент, который многие упускают: если адрес соединения не входит в set_real_ip_from, модуль не делает ничего. Заголовок игнорируется целиком, $remote_addr остаётся как есть. Именно в этом и зашита защита от подделки, к ней вернёмся.

Что такое real_ip_recursive. Заголовок X-Forwarded-For - это не один адрес, а список через запятую, который каждый прокси по пути дополняет справа. Запрос прошёл клиент -> CDN -> твой внешний балансировщик -> внутренний nginx, и заголовок выглядит так:

Код: Выделить всё

X-Forwarded-For: 203.0.113.7, 198.51.100.10, 10.0.0.5
Здесь 203.0.113.7 - настоящий клиент, 198.51.100.10 - выходной адрес CDN, 10.0.0.5 - твой внешний балансировщик. При real_ip_recursive off (по умолчанию) nginx возьмёт последний адрес из списка и на этом успокоится - а это адрес прокси, не клиента. С real_ip_recursive on nginx идёт по списку справа налево и берёт первый адрес, который не входит в доверенные сети set_real_ip_from. То есть пропускает все свои прокси и находит первого "чужого" - это и есть клиент. За многослойной инфраструктурой recursive on почти всегда обязателен.

Спуфинг X-Forwarded-For: почему доверять можно только своим

Вот центральная мысль всего урока. Заголовок nginx x-forwarded-for - это просто текст, который кто угодно может прислать в запросе. Если твой сервер открыт в интернет напрямую (а его реальный IP рано или поздно утечёт - через DNS-историю, сертификаты, ошибки конфигов), злоумышленник подключится в обход CDN и пришлёт:

Код: Выделить всё

curl -H "X-Forwarded-For: 8.8.8.8" https://origin.example.com/admin
Теперь представь, что ты настроил realip без ограничений - например, доверяешь любому источнику. Тогда nginx послушно подменит $remote_addr на 8.8.8.8, и атакующий обойдёт rate limit (каждый запрос с нового фейкового IP), пролезет через geo-фильтр, а если у тебя авторизация по IP - притворится доверенной сетью. Один заголовок ломает сразу всё.

Защита ровно одна и она встроена в саму модель: set_real_ip_from должен содержать только подсети твоих реальных прокси и CDN. Тогда подмена сработает лишь для соединений, физически пришедших с этих адресов. Пакет напрямую от атакующего придёт с его собственного IP, который в доверенных сетях не значится - и его X-Forwarded-For будет проигнорирован полностью. $remote_addr останется настоящим адресом атакующего, и rate limit его поймает.
Золотое правило: доверяй заголовку nginx real_ip_header РОВНО настолько, насколько доверяешь TCP-источнику, который его прислал. Никогда не пиши set_real_ip_from 0.0.0.0/0. Это эквивалент "верю любому, кто скажет, что он клиент".
Из этого же следует, почему нельзя слепо пробрасывать клиентский X-Forwarded-For вглубь. Если внешний слой nginx уже восстановил клиента, то Connection и заголовки внутрь должны идти контролируемо. Хороший паттерн - внешний прокси ставит X-Real-IP $remote_addr (уже восстановленный) и добавляет себя в X-Forwarded-For через $proxy_add_x_forwarded_for, а внутренние слои доверяют только внешнему. В эталонном gateway ngx-trace-gateway проброс заголовков централизован в globals/proxy: туда вынесены X-Real-IP $remote_addr и X-Forwarded-For, чтобы каждый хоп добавлял ровно одно доверенное звено, а не переписывал чужие данные.

Практика: правильный realip за CDN и балансировщиком

Разберём боевой конфиг для типового случая - origin за Cloudflare, плюс внутренний балансировщик. Сначала глобально, в http-блоке (или в отдельном include, как globals/realip в эталонном проекте).

Код: Выделить всё

# --- доверенные сети: ТОЛЬКО наши прокси и CDN ---
# выходные диапазоны Cloudflare (обновляются, держи актуальными)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;       # их IPv6 тоже
# наш собственный внутренний балансировщик
set_real_ip_from 10.0.0.0/8;

# за Cloudflare надёжнее их собственный заголовок - он содержит
# ровно один адрес клиента и его нельзя замусорить промежуточными хопами
real_ip_header   CF-Connecting-IP;
real_ip_recursive on;
Почему за Cloudflare берём CF-Connecting-IP, а не X-Forwarded-For. CF-Connecting-IP всегда содержит один адрес - реальный IP клиента, как его увидел Cloudflare. Его не нужно разворачивать рекурсивно, и его сложнее запутать. X-Forwarded-For за CDN тоже работает, но это список, и если клиент сам прислал в нём мусор, а CDN его не вычистил - ты можешь зацепить не то. Для чистого случая "только nginx-прокси без CDN" нормально использовать X-Forwarded-For с recursive on.

Проверим, что подмена действительно произошла. Добавим диагностический заголовок и сравним с прямым запросом:

Код: Выделить всё

location = /whoami {
    add_header X-Seen-Client $remote_addr always;
    return 200 "remote=$remote_addr xff=$http_x_forwarded_for\n";
}
Запрос через Cloudflare покажет твой настоящий клиентский адрес в remote. А вот тест на спуфинг - бьём напрямую в origin, минуя CDN:

Код: Выделить всё

$ curl -s -H "X-Forwarded-For: 1.2.3.4" -H "CF-Connecting-IP: 1.2.3.4" \
    http://origin.example.com/whoami
remote=88.88.88.88 xff=1.2.3.4
Видишь? remote остался 88.88.88.88 - реальный адрес того, кто стучится напрямую, потому что он не в set_real_ip_from. Заголовки 1.2.3.4 проигнорированы. Это и есть рабочая защита: подделать клиентский nginx реальный ip снаружи не вышло.

PROXY protocol: реальный IP на уровне L4

HTTP-заголовки работают, только когда балансировщик разбирает HTTP (L7). Но если перед тобой стоит чисто транспортный балансировщик - L4-балансер, AWS NLB, HAProxy в режиме tcp, ssl-passthrough - он не трогает HTTP вообще, просто прокидывает байты TCP. Заголовок добавить некому. Здесь спасает PROXY protocol: балансировщик в самом начале TCP-соединения шлёт маленький служебный заголовок с реальными адресами клиента и сервера, ещё до TLS-хендшейка и до HTTP.

Включается так - параметр proxy_protocol в директиве listen:

Код: Выделить всё

server {
    listen 443 ssl proxy_protocol;     # ждём PROXY-заголовок в начале соединения
    http2 on;

    # источник, которому доверяем PROXY-заголовок - адрес L4-балансера
    set_real_ip_from   10.0.0.0/8;
    real_ip_header     proxy_protocol;  # брать клиента из PROXY protocol

    location / {
        proxy_pass http://backend;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
Тут nginx proxy_protocol даёт два эффекта. Во-первых, появляются переменные $proxy_protocol_addr и $proxy_protocol_port с настоящим клиентским адресом и портом. Во-вторых, real_ip_header proxy_protocol заставляет модуль realip взять адрес именно из этого заголовка и подменить $remote_addr - той же логикой доверия через set_real_ip_from.

Важная грабля безопасности: сервер с listen ... proxy_protocol обязан получать PROXY-заголовок в каждом соединении, иначе nginx порвёт соединение с ошибкой. Поэтому такой listen нельзя выставлять в открытый интернет - туда должны ходить только твои балансировщики. Обычные клиенты PROXY protocol не умеют и получат разрыв. Держи L4-listen за закрытым периметром.

Типичные грабли
  • set_real_ip_from 0.0.0.0/0 в проде. Самая опасная ошибка - доверие всем. Любой подделает свой IP одним заголовком. Только конкретные подсети прокси.
  • Забыли real_ip_recursive on за несколькими слоями. Тогда в $remote_addr попадёт адрес последнего прокси из цепочки, а не клиент. Симптом: в логах вместо клиентов адреса твоих же балансировщиков.
  • Устаревшие диапазоны CDN. Cloudflare и другие меняют сети. Если их свежий выходной IP не в set_real_ip_from, для части трафика подмена молча перестанет работать и ты увидишь адреса CDN. Обновляй списки (у Cloudflare они публикуются, многие автоматизируют синхронизацию по cron).
  • realip объявлен глубже, чем нужно. set_real_ip_from в http-блоке наследуется во все server/location - это удобно. Но если объявить в одном location, в других подмены не будет. Решай осознанно, обычно глобально.
  • Доверяете X-Forwarded-For там, где надёжнее CF-Connecting-IP. За Cloudflare многозвенный XFF можно замусорить с клиентской стороны. Их собственный однозначный заголовок безопаснее.
  • proxy_protocol-listen смотрит в интернет. Соединения без PROXY-заголовка будут рваться. Этот listen - только для балансировщиков.
  • Перепутали X-Real-IP и X-Forwarded-For при пробросе вглубь. Внутренние слои должны доверять адресу, который проставил твой внешний прокси, а не сырому заголовку от клиента.
Мини-лаба: ловим спуфинг руками

Повтори по шагам, чтобы увидеть и подмену, и защиту своими глазами.
  • Шаг 1. Подними простой server без realip. Сделай location /whoami как выше с выводом $remote_addr и $http_x_forwarded_for. Запусти nginx -t, потом перезагрузи.
  • Шаг 2. Сходи с одной машины: curl -H "X-Forwarded-For: 9.9.9.9" http://СЕРВЕР/whoami. Убедись, что remote показывает твой реальный адрес, а xff - 9.9.9.9. Это база: nginx заголовок не трогает.
  • Шаг 3. Включи в http-блоке set_real_ip_from С ПОДСЕТЬЮ, в которую входит адрес твоей тестовой машины, real_ip_header X-Forwarded-For, real_ip_recursive on. Перечитай конфиг.
  • Шаг 4. Повтори curl из шага 2. Теперь remote стал 9.9.9.9 - ты сам себе доверенный прокси, и подмена сработала. Так выглядит "доверяю кому попало".
  • Шаг 5. Поменяй set_real_ip_from на подсеть, в которую твоя машина НЕ входит (например 198.18.0.0/15). Перечитай конфиг и снова сделай тот же curl. remote вернулся к настоящему адресу, заголовок 9.9.9.9 проигнорирован. Это и есть защита от спуфинга в действии.
  • Шаг 6. Добавь limit_req_zone по $binary_remote_addr и зафиксируй: при доверии всем (шаг 4) ты сам себе обходишь лимит сменой заголовка, при правильных доверенных сетях (шаг 5) - нет. Вот зачем этот урок идёт до rate limit.
Контрольные вопросы
  • Запрос пришёл с адреса 10.0.0.5 (он в set_real_ip_from), заголовок X-Forwarded-For: 203.0.113.7, 10.0.0.5, real_ip_recursive on. Какой адрес окажется в $remote_addr и почему?
  • Почему set_real_ip_from 0.0.0.0/0 полностью убивает защиту от подделки клиентского IP?
  • В каком случае HTTP-заголовки (XFF/CF-Connecting-IP) вообще не помогут восстановить клиента и придётся включать PROXY protocol?
  • Почему server с listen ... proxy_protocol опасно открывать в публичный интернет?
Итог

$remote_addr за прокси - это адрес соседа, а не клиента, и пока ты это не починил, твои логи, rate limit, geo и auth по IP работают по ложным данным. Модуль realip решает задачу тремя директивами: set_real_ip_from задаёт доверенные сети прокси и CDN, real_ip_header указывает источник настоящего адреса (X-Forwarded-For, CF-Connecting-IP или proxy_protocol), real_ip_recursive on разворачивает цепочку хопов до первого чужого. Вся безопасность держится на одном принципе - доверять заголовку ровно настолько, насколько доверяешь TCP-источнику, и никогда не шире своих подсетей. Для чисто транспортных балансировщиков клиентский адрес приходит через PROXY protocol на уровне L4. Сделай этот фундамент правильно сейчас - и все следующие уроки про лимиты, логи и доступ будут стоять на правде.
👍1 ❤️2 🔥2 😄 🤔1
Аватара пользователя
grumpydaemon
Сообщения: 1
Зарегистрирован: 31 май 2026, 09:08

Re: Модель доверия прокси-заголовков: realip и защита от спуфинга

Сообщение grumpydaemon »

А если у меня и L4-балансер с proxy_protocol, и сверху Cloudflare по HTTP - как совмещать? Получается на внешнем nginx real_ip_header proxy_protocol, а CF-Connecting-IP уже не зацепить? Или городить два слоя?
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
Srikant
Сообщения: 1
Зарегистрирован: 29 май 2026, 08:47

Re: Модель доверия прокси-заголовков: realip и защита от спуфинга

Сообщение Srikant »

Дошло наконец почему у меня rate limit ловил всех скопом - сидел за двумя nginx без real_ip_recursive on, в логах был адрес внутреннего балансера. Поставил recursive - клиенты появились, спасибо.
👍 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
Работа с заголовками: add_header и proxy_set_header
Следующая глава →
Сжатие: gzip, brotli и zstd

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Безопасность nginx: заголовки и hardeningЗаголовки в nginx: add_header и proxy_set_headerrealip в nginx: реальный IP за прокси и CDNStream в nginx: проксирование TCP и UDP

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя