Всплеск запросов к форме входа, 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 и поиска.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте