Rate limiting в Nginx и защита от DDoS

9 мин чтения·Matthieu·fail2banddos-protectionrate-limitingsecuritynginx|

Настройте 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:

  1. Добавьте limit_req_dry_run on; в конфигурацию
  2. Перезагрузите Nginx
  3. Сгенерируйте тестовый трафик (см. раздел тестирования ниже)
  4. Проверьте лог ошибок на записи 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; решает эту задачу.

  1. Проверьте 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.

  1. Когда скорости выглядят правильно, удалите строку 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