Всплеск запросов к форме входа, API или поиску — обычное дело. Иногда это наплыв пользователей, иногда — автоматизированный подбор паролей. Полностью блокировать адрес нельзя: за одним IP могут стоять сотни законопослушных клиентов. Nginx предлагает компромисс — модуль limit_req, который входит в пакеты основных дистрибутивов.
Зачем нужен лимит на уровне веб-сервера
Приложение тоже считает запросы, но уже после TLS, разбора заголовков и запуска обработчика. При переборе дорогая часть — проверка хеша и обращение к базе. Nginx отсекает лишнее раньше: не отдаёт запрос бэкенду и не занимает рабочий процесс. Это дешёвая первая линия обороны, которая дополняет ограничения в приложении и fail2ban.
Важно понимать границу: limit_req считает все запросы с выбранным ключом, а не «попытки входа». Если ключ — адрес клиента, лимит будет общим для всех вкладок. Поэтому зоны делают узкими: отдельно для входа, API и поиска.
Как работает алгоритм корзины
Nginx использует модель протекающего ведра. У каждого ключа есть счётчик и виртуальная очередь. Запросы приходят со своей скоростью, а «вытекают» с заданной rate. Если очередь переполнена, новый запрос получает отказ. Параметр burst задаёт размер очереди и позволяет пережить короткий всплеск. Без nodelay запросы притормаживаются, с nodelay отдаются сразу, но запас быстро расходуется.
Ключ зоны — не обязательно IP. Это может быть адрес вместе с именем хоста или идентификатор из cookie. Чем точнее ключ, тем меньше страдают соседи. Но нельзя брать произвольный заголовок от клиента: подставляя новые значения, злоумышленник обойдёт лимит.
Базовая зона для формы входа
Зона объявляется на уровне http, а применяется внутри location. В примере ниже разрешено пять запросов в минуту с небольшим запасом на загрузку страницы. Ответ 429 понятнее для клиента, чем стандартный 503: он прямо говорит, что превышен лимит, а не сломался сервер.
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
listen 443 ssl;
server_name example.com;
location = /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
limit_req_log_level notice;
proxy_pass http://app_backend;
}
}
}
Зона на 10 МБ хранит примерно 16 тысяч состояний. Этого хватает небольшому сайту. Если адресов больше, увеличьте размер: память расходуется линейно. Ключ $binary_remote_addr компактнее строкового и корректно работает с IPv6.
Отдельные лимиты для API и статики
API обычно требует более высокого лимита, чем вход, а статику вообще нет смысла ограничивать тем же счётчиком. Раздельные зоны позволяют задать разные пороги и не смешивать поведение. Для методов, которые меняют данные, имеет смысл сделать отдельную зону с меньшим rate, а для чтения — с большим.
limit_req_zone $binary_remote_addr zone=api_read:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=api_write:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=search:10m rate=2r/s;
location /api/ {
limit_req zone=api_read burst=60 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
}
location /api/orders {
limit_req zone=api_write burst=10 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
}
Не ставьте слишком маленький burst на страницы с множеством ресурсов: браузер открывает несколько соединений параллельно. Ориентируйтесь на реальный профиль сессии.
Исключения для офиса и мониторинга
Внутренние адреса, системы мониторинга и офисный NAT не должны попадать под общий лимит. Проще всего обнулить ключ для доверенных сетей через geo и map: пустой ключ означает, что запрос не учитывается. Список доверенных подсетей пересматривайте при изменении инфраструктуры.
geo $trusted_net {
default 0;
10.0.0.0/8 1;
192.168.0.0/16 1;
203.0.113.10/32 1;
}
map $trusted_net $limit_key {
0 $binary_remote_addr;
1 "";
}
limit_req_zone $limit_key zone=public:10m rate=10r/s;
Если сайт стоит за CDN или reverse proxy, $binary_remote_addr будет адресом прокси, и лимит сработает для всех сразу. Настройте real_ip с доверенными прокси, иначе клиент подделает заголовок и получит чужой ключ.
Коды ответа и журналирование
По умолчанию отклонённый запрос получает 503. Для API и публичных форм лучше 429, а для внутренних сервисов можно оставить 503, если клиентская библиотека уже умеет его обрабатывать. Полезно писать в лог результат работы лимита: переменная $limit_req_status показывает PASSED, DELAYED или REJECTED. Это помогает отличить реальную атаку от слишком строгой настройки.
log_format limits '$remote_addr [$time_local] "$request" '
'$status limit=$limit_req_status '
'host=$host request_time=$request_time';
access_log /var/log/nginx/limits.log limits;
error_log /var/log/nginx/error.log notice;
Не включайте подробный лог навсегда на нагруженном сайте: запись на диск становится узким местом. Достаточно отдельного файла с ротацией. Для проверки без отказов используйте limit_req_dry_run on: Nginx посчитает лимиты, но пропустит запросы.
Связка с fail2ban
Лимит замедляет перебор, но не блокирует источник. Fail2ban читает журнал и временно банит адреса, которые слишком часто попадают под ограничение. Связка получается экономной: Nginx отсекает лишние запросы, а fail2ban добавляет правило в межсетевой экран только для упорных нарушителей.
# /etc/fail2ban/filter.d/nginx-limit-req.conf
[Definition]
failregex = limiting requests, excess: .* by zone .*, client: <HOST>
ignoreregex =
# /etc/fail2ban/jail.d/nginx-limit-req.local
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 10m
bantime = 1h
Порог maxretry подбирайте по трафику. Слишком агрессивное правило забанит офис за NAT после одной активной сессии. Начните с наблюдения, затем ужесточайте.
Проверка под нагрузкой
После reload не ограничивайтесь одним curl. Прогоните серию запросов и убедитесь, что первые проходят, затем появляется 429, а после паузы доступ возвращается. Для API проверьте, что лимит не мешает штатному клиенту, который делает десятки вызовов подряд.
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/login
done
ab -n 200 -c 10 https://example.com/api/items
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
Смотрите не только на коды, но и на задержки: burst без nodelay добавляет ожидание. Сравните p95 времени ответа до и после включения лимита.
Чек-лист перед включением
- Зона объявлена в http, а не внутри server.
- Для формы входа и API используются разные зоны.
- Настроен real_ip, если перед Nginx стоит прокси или CDN.
- Код отказа соответствует ожиданиям клиентов: 429 или 503.
- Доверенные сети и мониторинг исключены осознанно.
- Проверены burst, задержки и поведение после паузы.
Итог
limit_req — простой инструмент, если рассматривать его как часть общей схемы. Он снижает нагрузку от перебора, но не заменяет проверку паролей и многофакторную аутентификацию. Начните с одной зоны для входа, снимите метрики, затем добавляйте лимиты для API и поиска.