1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 2 из 6

realip: вернуть настоящий адрес клиента

Коротко

X-Real-IP бэкенду - половина дела: сам nginx по-прежнему считает клиентом того, кто открыл соединение. Логи, лимиты и allow/deny смотрят на него. Подменяет адрес модуль realip: в официальных сборках он есть, проверяется nginx -V 2>&1 | grep -o with-http_realip_module.

  • Нужны set_real_ip_from со списком тех, кому веришь, и real_ip_header.
  • real_ip_recursive off (умолчание) берёт последний адрес цепочки - тот, что вписал твой прокси. Подделать его клиент не может.
  • real_ip_recursive on идёт справа налево мимо доверенных. С широким списком доверия это дыра: клиент назначает себе любой адрес одной строкой.
  • Директивы ставят на уровне http или server. В location они срабатывают не для всех фаз.

Если знаешь, чем real_ip_recursive on отличается от off и почему это вопрос безопасности - листай до «Теперь сам».

Сначала ответь сам

Перед nginx стоит балансировщик (он распределяет запросы между серверами). В set_real_ip_from коллега вписал не его адрес, а широкую подсеть, куда попали и клиенты. Клиент посылает руками заголовок X-Forwarded-For: 8.8.8.8. Балансировщик дописывает к нему настоящий адрес клиента и передаёт дальше.

Чей адрес запишет nginx в логи при real_ip_recursive on и при off?

При off - настоящий адрес клиента: он последний в цепочке, его вписал балансировщик. При on - 8.8.8.8, то есть тот, который клиент сам себе назначил: обход справа налево счёл настоящий адрес доверенным и дошёл до подделки. Со списком из одного балансировщика оба режима дали бы настоящий адрес. Замер ниже; ровно поэтому «включи recursive on» из большинства руководств стоит читать осторожно.

На что это похоже

Пропускной пункт с журналом. Внутрь ездит служебный автобус, и охранник записывает в журнал того, кто открыл дверь, - водителя. Все пассажиры в журнале неразличимы: сколько бы человек ни приехало, запись одна, водительская.

Чтобы различать пассажиров, вводят список: водитель отдаёт охраннику список тех, кого привёз. Работает это ровно до тех пор, пока список пишет водитель. Если охранник соглашается читать бумажку, которую принёс с собой пассажир, журнал перестаёт значить что-либо.

Механизм: цепочка адресов и граница доверия

X-Forwarded-For, как он пришёл 8.8.8.8 203.0.113.9 10.0.0.5 написал клиент вписал прокси доверенный recursive off: берём последний 10.0.0.5 - адрес узла, а не клиента, зато не подделать recursive on: идём влево мимо доверенных 203.0.113.9; при слишком широком списке дошли бы до 8.8.8.8
Граница доверия проходит по списку set_real_ip_from. Всё левее неё пишет кто угодно.

Проверено на стенде, список доверия - только 10.0.0.5: при off адресом стал 10.0.0.5, при on - 203.0.113.9, а с подсетью, куда попал и 203.0.113.9, on выдал 8.8.8.8.

Модуль срабатывает рано: подменённый адрес видят и логи, и limit_req, и allow/deny, и $proxy_add_x_forwarded_for. Настоящий адрес соединения при этом не теряется - он остаётся в $realip_remote_addr.

Правило

/etc/nginx/conf.d/shop.local.confhttp server
set_real_ip_from 10.0.0.5;         # адрес балансировщика, а не вся его подсеть
real_ip_header   X-Forwarded-For;

Список доверия пишут как можно уже: перечисляют сами прокси, а не сеть, в которой они стоят. Ширина этого списка и есть граница безопасности.

real_ip_recursive on включают только тогда, когда доверенных звеньев несколько и их число меняется. Во всех остальных случаях умолчание off строже: оно берёт ровно тот адрес, который вписал твой собственный прокси.

Разбор: что берётся из цепочки

Стенд, два блока, отличающихся одной строкой. Список доверия - 172.24.0.0/16 (запись /16 означает «первые 16 бит адреса совпадают», то есть все 172.24.x.x; один адрес пишут без неё):

X-Forwarded-For off даёт on даёт
203.0.113.9, 172.24.0.99 172.24.0.99 203.0.113.9
203.0.113.9 203.0.113.9 203.0.113.9
8.8.8.8, 9.9.9.9, 172.24.0.99 172.24.0.99 9.9.9.9
заголовка нет адрес соединения адрес соединения

Первая строка показывает беду off при широком списке: последним стоит адрес доверенного узла, и в логи попадает он. Вторая строка объясняет, почему ошибку редко замечают: при одном звене в цепочке обе настройки дают одно и то же.

Разбор: подделка

Цепочка из двух nginx: внешний передаёт X-Forwarded-For $proxy_add_x_forwarded_for, внутренний доверяет и подменяет адрес. Клиент приходит честно и приходит с подделанным заголовком:

список доверия recursive честный клиент клиент прислал 8.8.8.8
172.24.0.0/16 off 172.24.0.5 172.24.0.5
172.24.0.0/16 on 172.24.0.5 8.8.8.8
адрес внешнего nginx on 172.24.0.5 172.24.0.5

Разница между второй и третьей строкой - только ширина списка. Во второй в него попал и сам клиент: он сидит в той же сети 172.24.0.0/16. Поэтому обход справа налево пропустил его настоящий адрес как «доверенный» и добрался до подделки.

Отсюда правило целиком: recursive on превращает лишнюю широту списка доверия в дыру. С off та же широта даёт неверный, но неподделываемый адрес - в логах будет узел CDN, а не клиент, и это заметят.

Что ломается без этого

За CDN в логах два-три адреса на весь трафик:

У CDN узлов несколько, и в set_real_ip_from перечисляют все - каждый своей строкой.

↳  выводawk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -3
  48213 10.0.0.5
  47990 10.0.0.6
     11 192.168.1.44

limit_req по такому ключу ограничивает не пользователя, а всю CDN разом: первый же всплеск трафика перекрывает кислород всем сразу. deny от злоупотребления отрезает тоже всех. Расследование инцидента упирается в то, что адресов в логе просто нет.

Есть и обратная беда - set_real_ip_from 0.0.0.0/0;. Это «верить заголовку от кого угодно»: лимит обходится одной строкой в curl, бан по адресу не работает, а в логах записано то, что нападавший захотел там видеть.

Отдельная тонкость - место директив. Ставь их в http или server. В location подмена срабатывает не для всех фазРазбирается в разделе 13, глава «return против rewrite»: Фазы: кто и когда решает - шагов обработки запроса: блок с return 200 $remote_addr внутри такого location покажет старый адрес, а proxy_pass рядом - уже новый. Разбираться в этом расхождении посреди инцидента незачем.

Зачем это в работе

Первое, что стоит сделать после настройки, - логировать оба адреса рядом:

/etc/nginx/nginx.confhttp
log_format real '$remote_addr real=$realip_remote_addr xff="$http_x_forwarded_for" '
                '"$request" $status';
access_log /var/log/nginx/access.log real;

Имя формата своё, а не main: в официальном образе main уже объявлен. На стенде роль балансировщика играет сам 127.0.0.1, и он в списке доверия:

↳  выводtail -n 2 /var/log/nginx/access.log
203.0.113.9 real=127.0.0.1 xff="203.0.113.9" "GET /catalog/42 HTTP/1.1" 200
127.0.0.1 real=127.0.0.1 xff="-" "GET /health HTTP/1.1" 200

Первая строка: подмена сработала, соединение пришло от балансировщика. Вторая: запрос пришёл напрямую, заголовка нет, адрес остался своим. Такой формат сразу отвечает на вопрос «мимо CDN ходят или нет» - а ходят почти всегда, и это отдельный разговор про то, зачем закрывать сервер от прямых обращений.

Ещё одна мелочь, которую видно только в дампе: после подмены $proxy_add_x_forwarded_for дописывает к цепочке уже подменённый адрес, и бэкенд получает 203.0.113.9, 203.0.113.9 - замер дампом на бэкенде. Дубль безобиден, но если приложение разбирает цепочку само, знать о нём стоит.

Теперь сам

Перед твоим nginx стоит один балансировщик с фиксированным адресом. Коллега предлагает конфиг: set_real_ip_from 10.0.0.0/8; и real_ip_recursive on;. Что здесь не так и как переписать?

Не так обе строки сразу. Подсеть 10.0.0.0/8 - это шестнадцать миллионов адресов, и если хоть какие-то клиенты приходят изнутри неё, обход справа налево их пропустит и возьмёт то, что прислано клиентом. Звено одно, значит обход не нужен вовсе. Правильно: set_real_ip_from 10.0.0.5; с адресом самого балансировщика и умолчание off.

Главное

realip подменяет $remote_addr до логов, лимитов и правил доступа; настоящий адрес соединения остаётся в $realip_remote_addr, и логировать стоит оба. Нужны set_real_ip_from с максимально узким списком и real_ip_header. Умолчание real_ip_recursive off берёт последний адрес цепочки - его вписал твой прокси, и подделать его нельзя. on идёт влево мимо доверенных и вместе с широким списком отдаёт выбор адреса клиенту. Директивы ставят в http или server, а не в location.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.