Дефолтные фильтры fail2ban в репозитории не обновлялись толком с эпохи nginx 1.10. Они не учитывают ни HTTP/2 (где в access.log пишется HTTP/2.0), ни HTTP/3, ни новые форматы логов с real_ip за CDN. Я переписал свои фильтры пару лет назад и с тех пор они спокойно работают — расскажу, что и зачем.

Базовый log_format

Перед регексами — формат лога. Дефолтный combined слишком жирный и неудобный для парсинга. Я использую такой:

log_format main '$remote_addr $http_x_real_ip [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '$request_time $upstream_response_time $server_protocol';

access_log /var/log/nginx/access.log main;

Тут $remote_addr — реальный IP клиента (если nginx торчит наружу) или IP CDN-узла (если за прокси). $http_x_real_ip — IP, который пришёл в заголовке X-Real-IP или X-Forwarded-For от вышестоящего прокси. Это пригодится в фильтрах.

Фильтр на 4xx-флуд

Самое полезное правило — банить IP, который генерит много 404/403 за короткое время. Это и сканеры путей, и брутфорсеры WP-логинов, и просто косячные клиенты.

# /etc/fail2ban/filter.d/nginx-4xx.conf
[Definition]
failregex = ^<HOST>\s+\S+\s+\[.+\]\s+"(GET|POST|HEAD|PUT|DELETE)\s+\S+\s+HTTP/[\d.]+"\s+(401|403|404|444)\s
ignoreregex = ^<HOST>\s+\S+\s+\[.+\]\s+"(GET|POST|HEAD)\s+/(favicon\.ico|robots\.txt|sitemap\.xml)
datepattern = \[%%d/%%b/%%Y:%%H:%%M:%%S %%z\]

<HOST> — это спецтокен fail2ban для IP, он автоматом разворачивается в IPv4/IPv6-совместимый паттерн. HTTP/[\d.]+ ловит и HTTP/1.0, и HTTP/1.1, и HTTP/2.0, и HTTP/3.0.

ignoreregex исключает «нормальные» 404 — favicon, robots, sitemap. Без него вы будете банить браузеры, которые ходят за favicon.ico и не находят.

Jail:

# /etc/fail2ban/jail.d/nginx.conf
[nginx-4xx]
enabled = true
filter = nginx-4xx
logpath = /var/log/nginx/access.log
maxretry = 30
findtime = 60
bantime = 3600
action = iptables-multiport[name=nginx-4xx, port="http,https", protocol=tcp]

30 4xx-ответов за минуту = бан на час. Цифры подбирайте под свой трафик.

Фильтр на сканеры путей

Этот вылавливает попытки достучаться до типичных уязвимых путей. Это карательное правило с низким порогом срабатывания.

# /etc/fail2ban/filter.d/nginx-scanner.conf
[Definition]
failregex = ^<HOST>\s+\S+\s+\[.+\]\s+"(GET|POST|HEAD)\s+/(wp-login|wp-admin|administrator|phpmyadmin|\.env|\.git/|owa/|ecp/|autodiscover|boaform|cgi-bin/luci|HNAP1|GponForm|api/jsonws|jsp/login|console/login|manager/html|solr/|actuator/env|nice%%20ports|cf_scripts|cgi-bin/test|cmd\.php)
ignoreregex =
datepattern = \[%%d/%%b/%%Y:%%H:%%M:%%S %%z\]

Срабатывание на одном таком запросе — мгновенный бан. У меня jail настроен так:

[nginx-scanner]
enabled = true
filter = nginx-scanner
logpath = /var/log/nginx/access.log
maxretry = 1
findtime = 60
bantime = 86400

Час за один WP-сканер. Жёстко, но эффективно.

Фильтр на UA-фильтр

Боты с одинаковыми UA, которые ходят сериями — отдельная категория.

# /etc/fail2ban/filter.d/nginx-badbot.conf
[Definition]
failregex = ^<HOST>\s+.+"(MJ12bot|AhrefsBot|SemrushBot|DotBot|PetalBot|Bytespider|YandexBot|MauiBot|BLEXBot|DataForSeoBot|Megalodon|Mb2345|Sogou|Crawler)"
ignoreregex =

Тут жёстко — UA попал в список, IP в бан на сутки. Поправляйте список под себя, не все из этих ботов однозначно плохие.

Грабли: real_ip за прокси

Если у вас фронт стоит за CDN или другим nginx, то $remote_addr будет IP прокси, и fail2ban будет банить вышестоящий узел. Это катастрофа: положили CDN — положили весь трафик.

Лечится двумя путями.

Путь 1: настроить set_real_ip_from в nginx, чтобы $remote_addr стал клиентским IP:

set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

После этого в логе $remote_addr будет настоящий клиент, и fail2ban будет банить правильный IP.

Путь 2: парсить $http_x_real_ip или $http_x_forwarded_for напрямую в регексе. Но тут проблема: <HOST> в fail2ban стандартно ищет первую IP-подобную строку. Можно использовать usedns = no и кастомный паттерн.

Я использую путь 1 — он чище.

Логи в systemd

Если nginx пишет в systemd-journal, а не в файл — fail2ban тоже умеет:

[nginx-4xx]
enabled = true
filter = nginx-4xx
backend = systemd
journalmatch = _SYSTEMD_UNIT=nginx.service
maxretry = 30
findtime = 60
bantime = 3600

Whitelist обязательно

Никогда не запускайте fail2ban без ignoreip. Иначе в первый же день забаните себя.

# /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 1.2.3.4

В 1.2.3.4 — ваш домашний/офисный IP. Если динамический — добавляйте по диапазону или whitelist через домен (есть ignoreself = true).

Тестирование регексов

Перед тем как залить в прод — обязательно прогнать через fail2ban-regex:

fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf

Покажет, сколько строк подошло, сколько проигнорировано, и где <HOST> нашёл IP. Если в итогах 0 совпадений — регекс кривой.

Итог

Дефолтные фильтры fail2ban в 2025-2026 году бесполезны для большинства боевых сайтов. Свои писать несложно, главное — учесть реальный формат логов, прокси-цепочку и не забыть whitelist. И обязательно тестируйте через fail2ban-regex перед заливом в прод, иначе обнаружите кривой регекс уже после того, как jail заработает.