Ограничение скорости и защита от перебора: что выбрать
Один лимит по IP не защищает форму входа: пользователи сидят за общими адресами, а атака может распределяться по тысячам IP. Сравниваем Nginx, лимиты в приложении и Redis, а затем собираем рабочую схему.

Один лимит по IP не защищает форму входа: пользователь может сидеть за NAT, а перебор паролей — идти с тысяч разных адресов. Для веб-сервиса обычно приходится ограничивать сразу несколько сущностей: IP, учётную запись, устройство и сам endpoint. Ниже сравниваются три подхода — Nginx 1.25+, лимит внутри приложения и общий счётчик в Redis 7 — с их ценой, границами применимости и рабочей конфигурацией.
Сначала разделите две задачи
Rate limiting ограничивает частоту запросов. Например, не больше пяти попыток входа с одного IP за минуту. Защита от перебора отвечает на другой вопрос: как не дать злоумышленнику систематически проверять пароли, коды подтверждения или токены.
Эти механизмы пересекаются, но не заменяют друг друга. Лимит на IP легко обойти распределённой атакой. Лимит только на аккаунт может заблокировать настоящего владельца учётной записи. Практичная схема сочетает несколько ключей:
- IP-адрес — чтобы отсечь простой поток запросов до приложения;
- идентификатор аккаунта или нормализованный логин — чтобы остановить перебор одной учётной записи;
- связку «аккаунт плюс IP» — чтобы не заблокировать всех пользователей за одним NAT;
- endpoint и тип операции — для входа, восстановления пароля и отправки одноразового кода нужны разные лимиты.
Лимит по IP должен быть первым барьером, но не единственным решением.
Хороший rate limit не просто возвращает 429. Он должен остановить дешёвую атаку, не превратив общий мобильный интернет, офис или университетскую сеть в одного «нарушителя».
Три подхода и их цена
| Подход | Где работает | Плюсы | Цена и ограничения |
|---|---|---|---|
Nginx limit_req | На входе в сервис | Не нагружает приложение, прост в развёртывании, одинаково работает для всех backend-ов | Без дополнительной настройки видит в основном IP; состояние локально для worker-процесса и не подходит для сложных правил по аккаунту |
| Лимит в приложении | В обработчике входа или middleware | Доступны аккаунт, результат проверки пароля, роль и контекст операции | Запрос уже дошёл до приложения; при нескольких экземплярах нужен общий storage, иначе лимит обходится через балансировщик |
| Redis 7 как общий счётчик | Между всеми экземплярами приложения | Единое состояние, несколько ключей, короткие окна и атомарные операции | Появляется зависимость от Redis, нужно продумать TTL, отказоустойчивость и стоимость большого числа ключей |
Атомарность означает, что проверка и увеличение счётчика выполняются как одна неделимая операция. Иначе два параллельных запроса могут оба увидеть старое значение и вместе превысить лимит.
Вариант 1: отсекаем поток на Nginx
Для грубого ограничения публичного endpoint удобно начать с Nginx 1.25 или более новой версии. Модуль ngx_http_limit_req_module использует алгоритм leaky bucket[1]: запросы пропускаются с заданной скоростью, а короткие всплески помещаются в очередь burst.
Фрагмент ниже размещается внутри блока http. Он задаёт пять запросов в минуту на бинарный IP-адрес и разрешает короткую очередь из пяти запросов:
limit_req_zone $binary_remote_addr zone=login_by_ip:10m rate=5r/m;
upstream app_backend {
server 127.0.0.1:8000;
}
server {
listen 443 ssl;
server_name example.test;
location = /login {
limit_req zone=login_by_ip burst=5 nodelay;
limit_req_status 429;
proxy_pass http://app_backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Параметр nodelay пропускает запросы из разрешённой очереди сразу, но превышение лимита всё равно приводит к отказу. Без него Nginx может задерживать запросы, что для входа часто хуже: атакующий получает медленные ответы, а легитимный пользователь — лишнюю задержку.
Проверка результата: в access log должны появиться ответы со статусом 429 при серии запросов к /login. Не проверяйте это по одному запросу в браузере — браузер не создаёт воспроизводимую нагрузку.
Перед этим убедитесь, что Nginx правильно получает исходный адрес клиента. Если перед ним стоит CDN или другой reverse proxy, переменная $remote_addr может содержать адрес прокси. Настройка доверенных прокси и real IP — отдельная задача; без неё несколько миллионов пользователей могут выглядеть как один адрес.
Вариант 2: лимит внутри приложения
Приложение нужно подключать там, где появляется бизнес-контекст. Только оно знает, какой логин нормализовать, существует ли аккаунт и был ли пароль неверным. При этом нельзя возвращать разные ответы для «пользователь не найден» и «неверный пароль»: такая разница помогает перебирать базу логинов.
Для endpoint входа полезны как минимум три независимых решения:
- быстрый лимит по IP на reverse proxy;
- более строгий лимит по нормализованному логину;
- отдельный лимит на связку «логин плюс IP».
После превышения приложение может вернуть 429 и заголовок Retry-After, если клиенту действительно нужно знать время ожидания. Для формы входа не стоит сообщать точное число оставшихся попыток: эта информация помогает атакующему калибровать перебор.
Плюс подхода — возможность применить ступени: сначала задержка, затем CAPTCHA или подтверждение через почту, затем временная блокировка. Минус — запрос уже потратил соединение, worker приложения и часть ресурсов базы. Поэтому приложение не должно быть единственным барьером для шумного трафика.
Вариант 3: общий счётчик в Redis
Если сервис запущен в нескольких экземплярах, локальный словарь или память одного процесса не подходят. Пользователь может отправить первый запрос на instance A, второй на instance B и обойти ограничение. Redis 7 позволяет хранить счётчики в общем хранилище, а срок жизни ключа — через TTL.
Для простого фиксированного окна схема выглядит так: приложение формирует ключ вроде login:ip:203.0.113.10:2025-03-08T12:34, атомарно увеличивает счётчик, задаёт срок жизни ключа и отклоняет запрос после порога. В реальном коде увеличение и установка TTL должны выполняться атомарно: через транзакцию Redis или серверный Lua-скрипт. Конкретный способ зависит от клиента и его версии, поэтому имена методов нужно сверять с документацией используемой библиотеки.
Фиксированное окно легко объяснить, но у него есть граница: в момент смены минуты клиент может получить почти двойной лимит — в конце одного окна и в начале следующего. Если это критично, используйте скользящее окно или token bucket. Они точнее распределяют запросы, но требуют больше операций и памяти.
Для Redis отдельно задайте поведение при сбое. Если при недоступном Redis пропускать все запросы, атака продолжится без ограничений. Если запрещать все попытки входа, вы получите отказ в обслуживании для обычных пользователей. Часто разумный компромисс — оставить грубый лимит Nginx и временно применять консервативное локальное ограничение в приложении.
Как выбирать подход
Если сервис один и атака простая
Начните с Nginx по IP, а затем добавьте лимит по аккаунту в приложении. Это минимальная схема без новой инфраструктуры. Она не решает распределённый перебор полностью, но резко сокращает дешёвый поток запросов.
Если backend-ов несколько
Оставьте Nginx на внешнем контуре и вынесите счётчики по аккаунту и связке «аккаунт плюс IP» в Redis. Следите, чтобы все экземпляры приложения использовали одну Redis-базу или один согласованный кластер.
Если защищается отправка SMS или восстановление пароля
Считайте не только HTTP-запросы, но и дорогие побочные действия: отправленные сообщения, письма и созданные задания. Для таких операций лимит должен быть строже, чем для обычного просмотра страницы.
Что ломается чаще всего
- Лимит только по IP. Один офис или оператор связи получает общий адрес, а распределённая атака меняет адреса быстрее, чем срабатывает блокировка.
- Неправильный заголовок клиента. Значение
X-Forwarded-Forнельзя бездумно принимать от любого внешнего клиента. Доверяйте только адресам своих прокси. - Счётчик в памяти процесса. После перезапуска он обнуляется, а при нескольких instance состояние расходится.
- Блокировка по сырому логину. Разный регистр, пробелы и Unicode-нормализация позволяют обойти правило или создать лишние ключи.
- Тест только в браузере. Кэш, повторная отправка формы и клиентские задержки скрывают реальное поведение. Проверяйте access log, метрики 429 и распределение latency.
Что измерять до и после
Не называйте лимит «рабочим», пока не проверили его на нормальном трафике. Зафиксируйте долю 429 для обычных пользователей, число запросов к endpoint входа, p95 задержки, нагрузку на приложение и количество обращений к Redis. Тестируйте отдельно один IP с множеством логинов, один логин с разных IP и легитимный сценарий из общей сети.
Замеры нужно повторять на вашей инфраструктуре: результат зависит от числа Nginx worker-ов, размера зоны в памяти, количества экземпляров приложения, задержки до Redis и настроек балансировщика. Универсального безопасного числа запросов нет.
Что делать дальше
Сначала поставьте грубый лимит Nginx на endpoint входа и проверьте, что прокси видит настоящий IP. Затем добавьте в приложение ключи по аккаунту и по связке «аккаунт плюс IP». Если экземпляров несколько, перенесите счётчики в Redis и отдельно опишите поведение при его недоступности. После выката смотрите не только на число 429, но и на ложные блокировки, p95 задержки и всплески запросов к восстановлению пароля.
[1] Leaky bucket, «дырявое ведро», — алгоритм, который выпускает запросы с заданной скоростью и сглаживает короткие всплески, помещая их в ограниченную очередь.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: NGINX Documentation: ngx_http_limit_req_module, Redis Documentation: Rate Limiting
Частые вопросы
Достаточно ли поставить rate limit только на Nginx?
Какой лимит поставить для формы входа?
Почему локальный счётчик не работает при нескольких серверах?
Что произойдёт, если Redis станет недоступен?
Работает ли эта конфигурация на другой версии Nginx?
Нужно ли блокировать аккаунт после нескольких ошибок пароля?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.