Перейти к содержимому

X-forwarded-for: как защитить Ip-адрес клиента от подмены в nginx

В логе указан адрес, с которого запрос не приходил: его добавил сам клиент

Иногда система блокировки работает абсолютно корректно, но принимает неверное решение из-за данных, которым нельзя доверять. Типичный пример - правило: "после десяти неудачных попыток входа заблокировать IP-адрес". У одного из заказчиков такая защита начала банить адреса, с которых фактически не было ни одного запроса.

Сначала заблокировали один подозрительный IP, через пару часов - второй, затем третий. Все они относились к документационному диапазону, который не маршрутизируется в интернете. Значит, реальные подключения с этих адресов прийти не могли.

Проблема оказалась в источнике, из которого приложение брало IP пользователя. Логика блокировки была исправной, но сам адрес клиентского соединения определялся по HTTP-заголовку `X-Forwarded-For`. А этот заголовок пользователь может сформировать самостоятельно.

Почему `X-Forwarded-For` нельзя считать доказательством

`X-Forwarded-For` - обычное текстовое поле HTTP-запроса. Оно не имеет встроенной подписи, криптографической защиты или привязки к сетевому соединению. Любой клиент способен отправить, например:

```http
X-Forwarded-For: 203.0.113.1
```

Можно указать один адрес, список адресов или полностью выдуманную цепочку прокси:

```http
X-Forwarded-For: 10.10.10.10, 192.0.2.15, 203.0.113.1
```

Это не дефект `curl`, веб-сервера или конкретного фреймворка. Заголовок изначально создан для другой задачи: когда запрос проходит через прокси, исходный адрес может потеряться, поэтому промежуточный узел добавляет его в HTTP-заголовок.

Сам протокол не умеет отличать значение, записанное доверенным прокси, от строки, которую прислал злоумышленник. Такое различие появляется только благодаря настройке доверенной инфраструктуры.

Как формируется цепочка адресов

Прокси обычно не заменяет уже существующий `X-Forwarded-For`, а дописывает новый адрес в конец. В nginx для этого часто используется переменная `$proxy_add_x_forwarded_for`.

Допустим, клиент отправил:

```http
X-Forwarded-For: 203.0.113.1
```

После прохождения через ваш балансировщик запрос может попасть в приложение уже с таким значением:

```http
X-Forwarded-For: 203.0.113.1, 198.51.100.20
```

Левый элемент пришёл от клиента и может быть выдуман. Правый добавил прокси, который получил соединение от указанного источника.

Если в цепочке есть несколько контролируемых узлов, каждый из них добавляет по одному значению справа. Поэтому адрес пользователя нельзя бездумно брать ни как первый элемент, ни как последний.

При одном прокси последний адрес часто действительно совпадает с реальным IP клиента. Но при двух и более прокси последний элемент уже может быть адресом предыдущего промежуточного узла. В результате пользователи начинают выглядеть для приложения как один и тот же источник.

Корректный адрес определяется как элемент, расположенный на расстоянии N позиций от конца списка, где N - число доверенных прокси, которые действительно добавляют свои значения. При этом важно учитывать реальную конфигурацию: nginx не изменяет заголовок автоматически, если явно не настроено его формирование.

Настройка nginx: `set_real_ip_from` и `real_ip_recursive`

Для обработки подобных цепочек в nginx применяется модуль realip. Ключевую роль играют две директивы:

- `set_real_ip_from`;
- `real_ip_recursive`.

Первая задаёт адреса узлов, которым разрешено сообщать исходный IP клиента. Если соединение пришло не от доверенного источника, переданный заголовок не должен использоваться, а `$remote_addr` остаётся адресом сетевого соединения.

Пример:

```nginx
set_real_ip_from 198.51.100.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
```

Однако сама директива `real_ip_header` не создаёт доверенную модель. Если не ограничить список через `set_real_ip_from`, сервер фактически начинает принимать заявления об IP от любого отправителя.

Параметр `real_ip_recursive on` заставляет nginx разбирать цепочку справа налево. Доверенные адреса пропускаются, а первым недоверенным адресом считается клиентский.

При значении `off` nginx берёт только один элемент - крайний справа. Это может работать в простой схеме с одним прокси, но ломается при добавлении второго промежуточного узла.

Опасность слишком широкого доверия

Частая ошибка - добавить в `set_real_ip_from` всю частную сеть, например:

```nginx
set_real_ip_from 10.0.0.0/8;
```

На первый взгляд это удобно: внутренние прокси могут находиться в разных подсетях. Но такая настройка означает доверие любому устройству, которое способно подключиться к серверу из диапазона `10.0.0.0/8`.

Если приложение доступно из корпоративной сети, контейнерной инфраструктуры или соседних виртуальных сетей, любой внутренний сервис может отправить собственный `X-Forwarded-For` и повлиять на определение адреса клиента.

Безопаснее указывать только конкретные подсети или адреса реально используемых прокси. Список необходимо поддерживать вместе с изменениями инфраструктуры, а не составлять "на всякий случай" из всей внутренней адресации.

Облачные балансировщики и динамические адреса

С управляемыми балансировщиками ситуация сложнее. Их адреса могут меняться из-за масштабирования, обновлений и внутренних изменений платформы. Доверять отдельным IP-адресам бывает неудобно, а доверие ко всей подсети провайдера расширяет область риска.

Надёжный вариант - настроить внешний край так, чтобы он удалял клиентский `X-Forwarded-For` и создавал новый заголовок заново. Тогда приложение получает цепочку, сформированную только вашей инфраструктурой.

У разных поставщиков это может называться по-разному: режим перезаписи, удаления, сохранения или добавления заголовка. Иногда вместо `X-Forwarded-For` предоставляется отдельный заголовок, который балансировщик гарантированно формирует сам.

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

Отдельный заголовок как дополнительный вариант

Иногда внешний балансировщик можно настроить на передачу реального адреса через отдельный заголовок с заранее известным форматом. Это снижает риск случайного смешивания пользовательских данных с данными инфраструктуры.

Но одного необычного имени недостаточно. Если клиент способен обратиться к приложению напрямую и отправить этот заголовок самостоятельно, защиты не появится. Такой механизм работает только при одновременном выполнении двух условий:

1. приложение принимает запросы исключительно от доверенного внешнего узла;
2. внешний узел всегда удаляет входящее значение и записывает собственное.

Доступ к приложению в обход балансировщика следует закрыть сетевыми правилами, межсетевым экраном или политиками безопасности кластерной сети.

Почему неверный IP опасен не только для блокировок

Ошибочное определение адреса влияет не только на защиту от подбора паролей. На IP часто завязаны:

- ограничение частоты запросов;
- временные блокировки;
- аудит действий пользователей;
- географические правила;
- списки разрешённых адресов;
- расследование инцидентов;
- обнаружение автоматизированных атак;
- привязка сессий и подозрительных операций.

Если приложение принимает подставной адрес, злоумышленник может "повесить" свои действия на произвольный IP, вызвать блокировку невиновного пользователя или обойти лимит запросов, меняя значение заголовка.

Поэтому адрес, полученный из HTTP-заголовка, нельзя использовать как единственный фактор безопасности. Для критичных операций стоит учитывать также идентификатор учётной записи, токен, отпечаток сессии, сетевые признаки и историю поведения.

Как проверять конфигурацию на практике

Полезно составить схему прохождения запроса: клиент, CDN, внешний балансировщик, ingress, внутренний прокси, приложение. Для каждого звена нужно зафиксировать:

- какой адрес оно видит на сетевом уровне;
- какие заголовки получает;
- какие заголовки удаляет;
- какие добавляет;
- в каком порядке формируется список адресов;
- доступно ли приложение в обход этого узла.

Затем следует отправить тестовые запросы с заранее заданным `X-Forwarded-For` и проверить, что появляется в логах на каждом уровне. Такой тест должен выполняться как из внешней сети, так и из внутренних сегментов, поскольку именно внутренний доступ часто раскрывает ошибочное доверие.

Особое внимание стоит уделить IPv6, работе через CDN, контейнерным сетям и нескольким точкам входа. Конфигурация, корректная для одного маршрута, может давать иной результат при обращении через другой балансировщик.

Как правильно логировать адреса

Не стоит записывать в журнал только одно поле под названием `client_ip`. Лучше хранить несколько значений:

- фактический адрес TCP-соединения;
- результат обработки доверенного заголовка;
- исходный `X-Forwarded-For`;
- идентификатор балансировщика или прокси;
- имя точки входа;
- идентификатор запроса.

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

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

Что проверить в своей инфраструктуре

Начать аудит можно с короткого списка:

1. Найти все места, где приложение читает `X-Forwarded-For`, `X-Real-IP` и аналогичные поля.
2. Проверить, какие прокси считаются доверенными.
3. Убедиться, что прямой доступ к приложению закрыт.
4. Определить, кто удаляет пользовательские заголовки.
5. Проверить работу `real_ip_recursive`.
6. Сравнить фактическую цепочку с документацией инфраструктуры.
7. Протестировать запросы с подставными адресами.
8. Проверить лимиты, блокировки и аудит после изменения конфигурации.
9. Настроить отдельные предупреждения о невозможных или зарезервированных адресах.
10. Зафиксировать схему в документации и пересматривать её после изменений сети.

Главный принцип прост: клиент может сообщать о себе что угодно. Доверять можно только тому адресу, который подтверждён контролируемой цепочкой прокси, а сама эта цепочка должна быть ограничена, проверяема и защищена от прямого обхода.

Прокрутить вверх