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

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

Лимиты за прокси и другие пределы

Коротко

Лимит настроили, а он либо режет всех разом, либо не режет никого. Обе истории про ключ - и про пределы, которые к частоте отношения не имеют.

  • За CDN (сетью серверов-посредников перед твоим) $binary_remote_addr - это адрес CDN. Все клиенты сливаются в один ключ; лечит realip из седьмого раздела с узким списком доверия.
  • Брать ключ прямо из X-Forwarded-For нельзя: бот подставит случайную строку и получит своё пустое ведро на каждый запрос.
  • Пустое значение ключа выключает лимит - отсюда белые списки через map.
  • max_headers (1.29.8, умолчание 1000) закрывает дыру, которой раньше не было ограничения вовсе: запрос с 12 заголовками при max_headers 5 даёт 400.

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

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

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

Что общего у этих историй?

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

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

Пропускной режим по фамилии. Однофамильцы делят один лимит посещений: пришёл первый Иванов - остальным на сегодня нельзя. А если фамилию разрешено называть на слух и без документа, любой назовётся новой и пройдёт снова.

Ключ лимита - это и есть «фамилия». Он должен быть и общим для одного человека, и неподделываемым.

Механизм: где берётся адрес

клиенты CDN nginx $binary_remote_addr без realip адрес CDN - одно ведро на весь мир $http_x_forwarded_for как ключ бот пишет туда что угодно - ведро на каждый запрос
Правильный ключ - $binary_remote_addr ПОСЛЕ realip: адрес подставил доверенный прокси.

Правило

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

После этого $remote_addr - адрес клиента, и ключ снова осмыслен. Список доверия пишут узким, а real_ip_recursive не включают, пока звено одно: в седьмом разделе замерено, что с широким списком обход справа налево позволяет клиенту назначить себе любой адрес - то есть обойти этот самый лимит.

Не бери ключ прямо из заголовка

Соблазнительно написать limit_req_zone $http_x_forwarded_for zone=... и обойтись без realip. Так делать нельзя: заголовок присылает клиент, и бот подставит случайную строку на каждый запрос - у каждого будет свой ключ, своё пустое ведро и ноль ограничений. realip отличается принципиально: он доверяет заголовку только от перечисленных адресов.

Разбор: кого не считать вовсе

Пустое значение ключа выключает лимит для запроса. Отсюда стандартный приём - белый список через map:

/etc/nginx/nginx.confhttp
map $binary_remote_addr $limit_key {
    default      $binary_remote_addr;
    192.168.1.10 "";        # сервер мониторинга
}

limit_req_zone $limit_key zone=api:10m rate=10r/s;

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

Разбор: пределы, о которых забывают

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

директива умолчание что ограничивает ответ
client_max_body_size 1m размер тела 413
client_body_timeout 60s паузу при передаче тела 408
large_client_header_buffers 4 8k (4 буфера по 8 КБ) длину строки запроса и заголовка 414 / 400
max_headers 1000 число строк заголовков 400
keepalive_requests 1000 запросов на одно соединение закрытие

max_headers появилась только в 1.29.8 - до неё число заголовков не ограничивалось вовсе, и запрос с десятью тысячами строк был законным способом занять память воркера. Проверено:

$  команда
curl -s -o /dev/null -w '%{http_code}\n' -H 'X-1: a' -H 'X-2: b' http://localhost/
curl -s -o /dev/null -w '%{http_code}\n' $(for i in $(seq 12); do printf -- '-H X-%s:v ' $i; done) http://localhost/
↳  выводдва запроса при max_headers 5: с двумя заголовками и с двенадцатью
200
400

Обрати внимание: директива работает на уровне http или server. В location её не пустят - "max_headers" directive is not allowed here; на этом уровне заголовки уже разобраны.

Про client_max_body_size 1m вспоминают иначе - когда пользователи не могут загрузить фотографию и получают 413. Поднимать его на весь сайт не нужно: достаточно в том location, куда шлётся форма.

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

Стоит трезво понимать, чего этими лимитами не защитить. limit_req и limit_conn живут на седьмом, прикладном уровне (уровне самого HTTP): чтобы их применить, nginx уже принял соединение, разобрал заголовки и выбрал location.

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

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

Отдельно про worker_connections 1024 (умолчание): это не лимит клиента, а предел сервера. Он считает все соединения воркера, включая соединения к бэкендам, и упирание в него выглядит как строка worker_connections are not enough и отказ обслуживать вообще всех.

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

Порядок внедрения лимита на живом сайте: сначала limit_req_dry_run on на неделю, потом чтение лога, потом включение с кодом 429 и мониторингом доли отказов. Если после включения доля 429 выше долей процента - порог занижен, и ты режешь людей, а не ботов.

Проверить свой ключ можно за минуту: посмотри в логе распределение запросов по $binary_remote_addr за час. Если сверху сидят два-три адреса с гигантскими числами, а остальные почти пусты - у тебя перед nginx стоит прокси, и realip ещё не настроен.

Теперь сам

За nginx стоит CDN, realip настроен верно. На /api/ стоит limit_req с rate=10r/s, и всё работает. Добавили второй CDN-провайдер - и половина пользователей стала ловить 503. Что забыли?

Добавить адреса второго провайдера в set_real_ip_from. Для запросов из него подмена не срабатывает, $remote_addr остаётся адресом узла CDN, и все пользователи этого провайдера делят одно ведро. Признак в логе тот же: пара адресов с огромным числом запросов рядом с нормальным распределением остальных.

Главное

Ключ лимита должен быть общим для одного человека и неподделываемым. За CDN это $binary_remote_addr после realip с узким списком доверия; брать ключ прямо из X-Forwarded-For нельзя - бот подставит случайную строку и получит пустое ведро на каждый запрос. Пустое значение ключа выключает лимит, на этом строят белые списки через map. Рядом с частотой стоят пределы на сам запрос: client_max_body_size 1m, large_client_header_buffers, а с 1.29.8 - max_headers (умолчание 1000), закрывающая запросы с тысячами заголовков. И помни границу: эти лимиты отбивают одного назойливого клиента, а не объёмную или распределённую атаку.

Комментарии

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

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

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