Сайт за CDN: почему баны и лимиты не работали и как приложение доверяло чужому IP

Кейс из практики. Проект — финансовый онлайн-сервис с личными кабинетами, приёмом платежей, чатом поддержки и админ-панелью. Мы ведём его безопасность и инфраструктуру. Название проекта, адреса и даты не раскрываем; технические детали обобщены настолько, чтобы их нельзя было использовать для атаки.
Во время спам-атаки на чат поддержки мы блокировали адреса отправителя — и через минуту сообщения приходили снова. Блокировали в веб-сервере, блокировали в админке, лимиты частоты стояли на месте. Не помогало ничего.
Симптом: блокировки не работают
Сайт работал за CDN — сетью прокси, которая принимает запросы посетителей и пересылает их на сервер. В логах веб-сервера в поле адреса клиента стояли адреса узлов CDN: их сотни, и каждый следующий запрос мог прийти через другой узел.
Как сервер за CDN видит клиента
Для сервера собеседник — узел CDN. Настоящий адрес посетителя CDN передаёт в отдельном заголовке. Веб-сервер должен знать, каким узлам можно верить, и только от них брать адрес из заголовка.
| Источник | Кто заполняет | Можно ли доверять |
|---|---|---|
| Адрес соединения без настройки | TCP | Да, но это адрес узла CDN |
| Заголовок CDN с адресом посетителя | CDN | Да — если запрос пришёл с адреса CDN |
X-Forwarded-For, первое значение | Клиент, затем прокси | Нет |
X-Forwarded-For, последнее значение | Ближайший прокси | Только если этот прокси ваш |
Две ошибки сразу
- Веб-сервер не знал про CDN. Определение реального адреса не было настроено, поэтому все лимиты считались по узлам — ни один не превышал порог, а бан адреса посетителя не срабатывал.
- Приложение доверяло заголовку от клиента. Веб-сервер дописывал адрес к заголовку
X-Forwarded-For, пришедшему от клиента, а приложение брало из него первое значение. Значит, чёрный список приложения мог проверять совсем не тот адрес.
Что исправили
# /etc/nginx/conf.d/cdn-realip.conf
set_real_ip_from 173.245.48.0/20; # … все опубликованные диапазоны CDN, v4 и v6
real_ip_header CF-Connecting-IP;
# во всех location с proxy_pass
proxy_set_header X-Forwarded-For $remote_addr; # вместо $proxy_add_x_forwarded_for
proxy_set_header X-Real-IP $remote_addr;- После этого веб-сервер видит настоящий адрес посетителя, и лимиты, баны и логи заработали.
- В приложении осталась одна функция определения адреса, которая читает только доверенный заголовок. Лимиты, чёрный список и геолокация используют её; собственное чтение заголовков в коде запрещено.
- Диапазоны CDN вынесены в отдельный файл с пометкой об источнике — их нужно обновлять.
- Прямой доступ к серверу в обход CDN нужно закрыть файрволом: разрешить 80/443 только из диапазонов CDN.
Как проверить свой сервер
- Посмотрите адреса клиентов в логе веб-сервера. Если там адреса CDN или балансировщика — real_ip не настроен.
- Найдите в коде все места, где читаются заголовки с адресом клиента, и сведите их к одной функции.
- Убедитесь, что сервер не отвечает на запросы по прямому IP мимо CDN.
Материалы для проверки
Если нужна реализация
Состав работ зависит от текущего проекта и подтверждённой причины. Можно обсудить диагностику или разработку подходящего решения.
Вопросы и ответы
Можно ли брать последний адрес из X-Forwarded-For?
Только если вы точно знаете, сколько доверенных прокси стоит перед приложением, и это число не меняется. Надёжнее настроить real_ip в веб-сервере по списку адресов CDN.
Почему важно закрыть доступ к серверу в обход CDN?
Иначе защита CDN обходится целиком, а проверка адресов теряет смысл. Входящие соединения на веб-порты стоит разрешать только из диапазонов CDN.