Rate limiting в Nginx и защита от DDoS
Настройте rate limiting в Nginx с помощью limit_req, limit_conn и fail2ban для защиты сервера от брутфорса и DDoS-атак на уровне приложения без сторонних сервисов.
Rate limiting — первая линия обороны от брутфорса, злоупотребления API и DDoS-атак на уровне приложения. В этом руководстве мы построим три уровня защиты, используя только Nginx и fail2ban. Без сторонних anti-DDoS-сервисов, без трафика, покидающего ваш сервер.
Вы настроите ограничение частоты запросов (limit_req), ограничение соединений (limit_conn) и автоматическую блокировку IP (fail2ban) на Debian 12 или Ubuntu 24.04.
Предварительные требования:
- Nginx установлен и запущен
- Базовое знакомство со структурой конфигурации Nginx
- Доступ root или sudo
Как работает rate limiting в Nginx?
Rate limiting в Nginx использует алгоритм «дырявое ведро» (leaky bucket) через директивы limit_req_zone и limit_req. Входящие запросы наполняют ведро с любой скоростью. Ведро опустошается с фиксированной скоростью, которую вы задаёте. Когда ведро переполняется, Nginx отклоняет лишние запросы. Это сглаживает всплески трафика, сохраняя постоянную скорость обработки.
Реализация включает две директивы. limit_req_zone определяет зону разделяемой памяти, которая отслеживает состояние каждого клиента во всех worker-процессах. limit_req применяет ограничение к конкретным location.
# В блоке http: определяем зону
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# В блоке server или location: применяем ограничение
limit_req zone=api;
Ключ $binary_remote_addr хранит каждый IP клиента в компактном бинарном формате (4 байта для IPv4, 16 байт для IPv6). Зона на 10 МБ вмещает примерно 160 000 IPv4-адресов или 80 000 IPv6-адресов. Для большинства серверов 10m более чем достаточно.
Параметр rate принимает запросы в секунду (r/s) или в минуту (r/m). Nginx отслеживает это внутренне в миллисекундах. Значение 10r/s означает один разрешённый запрос каждые 100 мс.
Как настроить limit_req_zone и limit_req?
Создайте отдельный файл конфигурации для rate limiting:
sudo nano /etc/nginx/conf.d/rate-limiting.conf
# Зоны разделяемой памяти — определяются на уровне http
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
# Возвращать 429 вместо дефолтного 503
limit_req_status 429;
# Логировать события rate limiting на уровне warn (задержки на уровне notice)
limit_req_log_level warn;
Затем примените зоны в вашем блоке server:
sudo nano /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
# Общий rate limit для всех запросов
limit_req zone=general burst=20 nodelay;
location /login {
# Строгий лимит на эндпоинт логина
limit_req zone=login burst=3 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
# Более высокий лимит для потребителей API
limit_req zone=api burst=50 delay=30;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Проверьте конфигурацию и перезагрузите:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Когда директива limit_req определена в блоке location, она перекрывает любой limit_req, унаследованный от уровня server. Зона general применяется к /, но не к /login и /api/, поскольку у этих location есть собственные директивы limit_req. Если нужно, чтобы обе зоны применялись, добавьте несколько строк limit_req в одном блоке.
Что делают burst, nodelay и delay?
Параметр burst определяет, сколько «лишних» запросов Nginx ставит в очередь вместо немедленного отклонения. Без burst любой запрос сверх нормы получает 429. С burst Nginx удерживает лишние запросы в очереди и выпускает их с базовой скоростью.
| Параметр | Мгновенные запросы | В очереди | Отклонённые | Применение |
|---|---|---|---|---|
burst=0 (по умолчанию) |
1 за интервал | Нет | Всё сверх нормы | Строгие API-лимиты |
burst=5 |
1 за интервал | До 5, выпускаются с базовой скоростью | Свыше burst+1 | Отправка форм |
burst=5 nodelay |
До 6 сразу | Нет в очереди, но слоты burst восстанавливаются с базовой скоростью | Свыше burst+1 до восстановления | Логин-страницы, общий трафик |
burst=20 delay=10 |
До 11 сразу | Запросы 12-21 замедляются до базовой скорости | Свыше burst+1 | API со случайными всплесками |
При burst=5 (без nodelay), если 6 запросов приходят одновременно, запрос 1 обрабатывается сразу. Запросы 2-6 ставятся в очередь и выпускаются по одному за интервал (каждые 100 мс при 10r/s). Последний запрос в очереди ждёт 500 мс. Это добавляет задержку, но не отбрасывает легитимные всплески.
При burst=5 nodelay все 6 запросов обрабатываются немедленно. Но 5 слотов burst восстанавливаются 500 мс. Если через 200 мс придут ещё 6 запросов, восстановятся только 3 слота, поэтому 3 лишних запроса будут отклонены.
При burst=20 delay=10 первые 11 запросов (1 базовый + 10 порог delay) обрабатываются без ожидания. Запросы 12-21 замедляются до базовой скорости. Всё свыше 21 отклоняется. Этот гибридный режим хорошо работает для API, получающих периодические всплески от легитимных batch-клиентов.
Как ограничить разные эндпоинты отдельно?
Определите отдельные зоны с разными ключами для независимых лимитов. Пример выше уже использует три зоны. Также можно ограничивать по URI-пути, используя $uri в качестве ключа:
# Rate limiting по URI: ограничивает общее количество запросов к каждому уникальному URI
limit_req_zone $uri zone=per_uri:10m rate=50r/s;
Это полезно, когда определённым эндпоинтам (вроде страницы поиска или функции экспорта) нужно глобальное ограничение вне зависимости от того, какой клиент их вызывает.
Для rate limiting по API-ключу используйте map для извлечения ключа из заголовка:
map $http_x_api_key $api_key_limit {
default $binary_remote_addr;
"~^.+$" $http_x_api_key;
}
limit_req_zone $api_key_limit zone=api_keyed:10m rate=100r/s;
Если клиент отправляет заголовок X-API-Key, rate limit привязывается к этому ключу. Иначе — откат к ограничению по IP.
Как ограничить соединения с limit_conn?
limit_req контролирует частоту запросов, а limit_conn ограничивает количество одновременных соединений от одного клиента. Хорошо работает против slowloris-атак и злоупотребления скачиванием.
# В блоке http
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# В блоке server или location
server {
# Максимум 20 одновременных соединений на IP
limit_conn addr 20;
location /downloads/ {
# Максимум 2 одновременных скачивания на IP
limit_conn addr 2;
limit_rate 1m; # Ограничить полосу до 1 МБ/с на соединение
}
}
Замечание по HTTP/2 и HTTP/3: каждый конкурентный запрос считается отдельным соединением. Браузер, загружающий страницу с 30 ресурсами через одно HTTP/2-соединение, считается как 30 соединений для limit_conn. Устанавливайте лимит выше, чем для HTTP/1.1.
limit_conn и limit_req дополняют друг друга. Используйте оба. limit_req останавливает очереди запросов. limit_conn останавливает потоки соединений.
Как вернуть кастомную страницу ошибки 429?
По умолчанию ограниченные запросы получают стандартную страницу ошибки. Кастомная 429-страница может включать заголовок Retry-After и понятное сообщение.
Создайте страницу ошибки:
sudo mkdir -p /var/www/error
sudo nano /var/www/error/429.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>429 - Too Many Requests</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 5rem 1rem; }
h1 { font-size: 2rem; }
p { color: #555; }
</style>
</head>
<body>
<h1>429 - Too Many Requests</h1>
<p>You have exceeded the request limit. Wait a moment and try again.</p>
</body>
</html>
sudo chmod 644 /var/www/error/429.html
Добавьте страницу ошибки и заголовок Retry-After в блок server:
server {
#... директивы rate limiting...
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
}
Директива internal запрещает прямой доступ к странице ошибки. Ключевое слово always в add_header гарантирует отправку заголовка даже в ответах с ошибкой. Значение Retry-After (в секундах) подсказывает корректным клиентам, когда повторить запрос.
Как безопасно протестировать rate limit с dry_run?
Включите limit_req_dry_run on для симуляции rate limiting без отклонения запросов. Nginx логирует, что бы он сделал, но все запросы проходят. Опция доступна начиная с Nginx 1.17.1.
server {
limit_req zone=general burst=20 nodelay;
limit_req_dry_run on; # Только логировать, не применять
# Добавить статус rate limiting в access-логи
#...
}
Добавьте $limit_req_status в формат лога для отслеживания dry run событий в access-логах:
# В блоке http
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# В блоке server
access_log /var/log/nginx/access.log ratelimit;
Процесс dry_run:
- Добавьте
limit_req_dry_run on;в конфигурацию - Перезагрузите Nginx
- Сгенерируйте тестовый трафик (см. раздел тестирования ниже)
- Проверьте лог ошибок на записи dry run:
sudo grep "dry run" /var/log/nginx/error.log
2026/03/19 14:22:31 [warn] 1234#1234: *567 limiting requests, dry run, excess: 1.532 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Уровень лога здесь [warn] из-за директивы limit_req_log_level warn. Убедитесь, что ваша директива error_log включает уровень warn или ниже, иначе эти сообщения не появятся. Продакшн-конфигурация с error_log /var/log/nginx/example.error.log warn; решает эту задачу.
- Проверьте access-логи на переменную
$limit_req_status:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log
Переменная $limit_req_status возвращает одно из значений: PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN или REJECTED_DRY_RUN.
- Когда скорости выглядят правильно, удалите строку
limit_req_dry_runи перезагрузите.
Как добавить доверенные IP в белый список?
Используйте блок geo, чтобы исключить системы мониторинга, балансировщики нагрузки или офисные IP из rate limiting:
# В блоке http
geo $limit {
default 1;
10.0.0.0/8 0; # Внутренняя сеть
192.168.0.0/16 0; # Внутренняя сеть
203.0.113.10 0; # Сервер мониторинга
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=general:10m rate=10r/s;
Когда $limit_key — пустая строка, Nginx полностью пропускает rate limiting для этого запроса. IP, совпадающие с блоком geo, получают $limit = 0, что отображается в пустой ключ.
Если вы хотите, чтобы IP из белого списка имели более высокую скорость вместо отсутствия ограничений:
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=trusted:1m rate=100r/s;
server {
limit_req zone=general burst=20 nodelay;
limit_req zone=trusted burst=100 nodelay;
}
Все IP попадают под trusted, но только IP не из белого списка попадают под general. Применяется самое строгое ограничение, поэтому IP из белого списка фактически ограничены до 100 r/s, а все остальные — до 10 r/s.
Как банить повторных нарушителей с fail2ban?
Rate limiting отклоняет отдельные запросы, но настойчивые атакующие возвращаются снова и снова. fail2ban мониторит лог ошибок Nginx и банит IP на уровне файрвола после повторных нарушений.
Установите fail2ban, если ещё не установлен:
sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-03-19 14:30:00 UTC; 2s ago
fail2ban включает встроенный фильтр nginx-limit-req. Регулярное выражение фильтра соответствует строкам вида:
limiting requests, excess: 1.532 by zone "general", client: 203.0.113.50
Создайте конфигурацию jail. Никогда не редактируйте .conf-файлы напрямую; используйте .local-оверрайды:
sudo nano /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 600
Это банит IP на 10 минут после 10 нарушений rate limiting за 60 секунд.
Для эскалации банов добавьте второй jail в тот же файл:
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 30
findtime = 3600
bantime = 86400
Первый jail ловит короткие всплески (10 попаданий за минуту = бан на 10 минут). Второй ловит настойчивых нарушителей (30 попаданий за час = бан на 24 часа).
Перезапустите fail2ban и проверьте статус jail:
sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req
На Debian 12 и более старых системах вывод выглядит так:
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/nginx/error.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
На Ubuntu 24.04 fail2ban по умолчанию использует бэкенд systemd journal (backend = auto разрешается в systemd). Вывод показывает Journal matches: вместо File list::
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=nginx.service + _COMM=nginx
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Оба бэкенда работают. Journal-бэкенд читает те же сообщения логов Nginx через systemd. Если предпочитаете мониторинг по файлам, добавьте backend = pyinotify в секцию jail.
Для ручного разбана IP во время тестирования:
sudo fail2ban-client set nginx-limit-req unbanip 203.0.113.50
Лог jail доступен через:
sudo journalctl -u fail2ban -f
Как проверить, что rate limiting работает?
Тестируйте с внешней машины. Не тестируйте с localhost, потому что 127.0.0.1 может быть в белом списке.
Быстрый тест с циклом curl:
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
done
При rate 10r/s и burst=20 nodelay первые 21 запрос вернут 200. Как только burst исчерпан, ответы переключаются на 429.
Нагрузочный тест с wrk:
sudo apt install -y wrk
wrk -t2 -c10 -d10s https://example.com/
Running 10s test @ https://example.com/
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.23ms 2.11ms 28.44ms 75.32%
Req/Sec 1.02k 121.33 1.34k 68.00%
20384 requests in 10.01s, 15.22MB read
Non-2xx or 3xx responses: 18241
Requests/sec: 2036.36
Transfer/sec: 1.52MB
Счётчик Non-2xx or 3xx responses показывает, сколько запросов Nginx ограничил. Здесь 18 241 из 20 384 запросов получили 429.
Проверьте лог ошибок во время теста:
sudo tail -f /var/log/nginx/error.log
2026/03/19 14:45:12 [warn] 1234#1234: *890 limiting requests, excess: 9.876 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Значение excess показывает, насколько запрос превысил лимит. Более высокие значения указывают на более агрессивный трафик.
Полная продакшн-конфигурация
Полная конфигурация rate limiting, объединяющая все три уровня:
# /etc/nginx/conf.d/rate-limiting.conf
# --- Белый список ---
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/16 0;
# Добавьте ваши IP мониторинга / офиса здесь
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
# --- Зоны частоты запросов ---
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=1r/s;
limit_req_zone $limit_key zone=api:10m rate=30r/s;
# --- Зона соединений ---
limit_conn_zone $binary_remote_addr zone=addr:10m;
# --- Коды ответов и логирование ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;
# --- Access-лог со статусом rate limiting ---
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.access.log ratelimit;
error_log /var/log/nginx/example.error.log warn;
# Глобальные ограничения
limit_req zone=general burst=20 nodelay;
limit_conn addr 30;
# Кастомная 429-страница
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
location /login {
limit_req zone=login burst=3 nodelay;
limit_conn addr 5;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=50 delay=30;
limit_conn addr 20;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 10
findtime = 60
bantime = 600
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 30
findtime = 3600
bantime = 86400
После создания всех файлов конфигурации:
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban
Для более полной настройки безопасности, включающей заголовки, TLS и другие меры усиления, см..
Справочник по директивам
| Директива | Контекст | По умолчанию | Начиная с |
|---|---|---|---|
limit_req_zone |
http | - | 0.7.21 |
limit_req |
http, server, location | - | 0.7.21 |
limit_req_status |
http, server, location | 503 | 1.3.15 |
limit_req_log_level |
http, server, location | error | 0.8.18 |
limit_req_dry_run |
http, server, location | off | 1.17.1 |
limit_conn_zone |
http | - | 1.1.8 |
limit_conn |
http, server, location | - | 0.7.21 |
limit_conn_status |
http, server, location | 503 | 1.3.15 |
limit_conn_log_level |
http, server, location | error | 0.8.18 |
limit_conn_dry_run |
http, server, location | off | 1.17.6 |
Устранение неполадок
Rate limiting вообще не работает: Проверьте, что limit_req_zone находится в блоке http, а не внутри блока server. Зоны должны быть определены до того, как на них ссылаются. Если конфигурация использует директивы include, убедитесь, что файл с зонами подключается до блоков server.
Легитимные пользователи получают 429: Снизьте rate, увеличьте burst или перейдите от режима без burst к burst=N nodelay. Используйте режим dry_run для измерения реальных паттернов трафика перед установкой лимитов. Проверьте, не раздувает ли мультиплексирование HTTP/2 счётчики limit_conn.
fail2ban не банит: Убедитесь, что logpath совпадает с реальным расположением error-логов Nginx. Проверьте, что limit_req_log_level установлен в warn или error (значение по умолчанию). Проверьте активность jail командой sudo fail2ban-client status nginx-limit-req. При тестировании с localhost учтите, что fail2ban по умолчанию игнорирует собственный IP сервера (ignoreself = true). Тестируйте с внешней машины.
Сообщения о rate limiting не появляются в логе ошибок: Директива error_log по умолчанию стоит на уровне error, что фильтрует warn-сообщения от limit_req_log_level warn. Установите error_log /var/log/nginx/error.log warn; в блоке server, чтобы видеть события rate limiting.
Память зоны исчерпана: Зона 10m вмещает около 160 000 состояний IPv4. Если в логах появляется could not allocate node, увеличьте размер зоны. Отслеживайте использование зоны через $limit_req_status в access-логах.
За балансировщиком нагрузки или CDN: Если Nginx видит только IP балансировщика, rate limiting применяется к этому единственному IP. Используйте $http_x_forwarded_for или $realip_remote_addr в качестве ключа зоны вместо $binary_remote_addr. Также нужно настроить set_real_ip_from с диапазоном IP балансировщика. Доверяйте X-Forwarded-For только от известных прокси, потому что клиенты могут его подделать.
Логи — ваш основной инструмент отладки:
# Отслеживание событий rate limiting в реальном времени
sudo tail -f /var/log/nginx/error.log | grep "limiting"
# Проверка действий fail2ban
sudo journalctl -u fail2ban -f
Авторское право 2026 Virtua.Cloud. Все права защищены. Данный контент является оригинальным произведением команды Virtua.Cloud. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.
Готовы попробовать?
Разверните свой сервер за секунды. Linux, Windows или FreeBSD.
Смотреть тарифы VPS