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
Готовы попробовать?
Разверните свой сервер за секунды. Linux, Windows или FreeBSD. →