# теория · шаг 3 из 6
Лимиты за прокси и другие пределы
Коротко
Лимит настроили, а он либо режет всех разом, либо не режет никого. Обе истории про ключ - и про пределы, которые к частоте отношения не имеют.
- За CDN (сетью серверов-посредников перед твоим)
$binary_remote_addr- это адрес CDN. Все клиенты сливаются в один ключ; лечитrealipиз седьмого раздела с узким списком доверия. - Брать ключ прямо из
X-Forwarded-Forнельзя: бот подставит случайную строку и получит своё пустое ведро на каждый запрос. - Пустое значение ключа выключает лимит - отсюда белые списки через
map. max_headers(1.29.8, умолчание 1000) закрывает дыру, которой раньше не было ограничения вовсе: запрос с 12 заголовками приmax_headers 5даёт 400.
Если знаешь, почему ключ нельзя брать из заголовка, - листай до «Теперь сам».
Сначала ответь сам
Лимит проверили на себе - работает. Через неделю две жалобы: пользователи из одного офиса ловят 503 пачками, хотя каждый заходит раз в минуту, а бот из интернета проходит сквозь лимит без сопротивления.
Что общего у этих историй?
Ключ. В первой все сотрудники офиса приходят с одного внешнего адреса и делят одно ведро. Во второй ключ взяли из заголовка, который присылает сам клиент, - и бот меняет его на каждом запросе.
На что это похоже
Пропускной режим по фамилии. Однофамильцы делят один лимит посещений: пришёл первый Иванов - остальным на сегодня нельзя. А если фамилию разрешено называть на слух и без документа, любой назовётся новой и пройдёт снова.
Ключ лимита - это и есть «фамилия». Он должен быть и общим для одного человека, и неподделываемым.
Механизм: где берётся адрес
Правило
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:
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/
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), закрывающая запросы с тысячами заголовков. И
помни границу: эти лимиты отбивают одного назойливого клиента, а не объёмную или
распределённую атаку.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий