# теория · шаг 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» из
большинства руководств стоит читать осторожно.
На что это похоже
Пропускной пункт с журналом. Внутрь ездит служебный автобус, и охранник записывает в журнал того, кто открыл дверь, - водителя. Все пассажиры в журнале неразличимы: сколько бы человек ни приехало, запись одна, водительская.
Чтобы различать пассажиров, вводят список: водитель отдаёт охраннику список тех, кого привёз. Работает это ровно до тех пор, пока список пишет водитель. Если охранник соглашается читать бумажку, которую принёс с собой пассажир, журнал перестаёт значить что-либо.
Механизм: цепочка адресов и граница доверия
Проверено на стенде, список доверия - только 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.
Правило
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 перечисляют все - каждый своей
строкой.
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 рядом - уже новый.
Разбираться в этом расхождении посреди инцидента незачем.
Зачем это в работе
Первое, что стоит сделать после настройки, - логировать оба адреса рядом:
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, и он в списке доверия:
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.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий