Fail2Ban, CSF и Suricata
Интеграции для сервера: автоматически отправлять жалобы из Fail2Ban, добавлять адреса из чёрного списка в CSF и подписывать алерты Suricata оценкой репутации. Готовые конфиги копируются целиком.
Раздел «API v2»: Обзор · Проверка IP (CHECK) · Жалобы по IP (REPORTS) · Отправить жалобу (REPORT) · Чёрный список (BLACKLIST) · Проверка подсети (CHECK-BLOCK) · Массовые жалобы (BULK-REPORT) · Удаление жалоб (CLEAR-ADDRESS) · Лимиты · Ошибки · Fail2Ban, CSF, Suricata · FAQ
Fail2Ban: отправка жалоб автоматически
Fail2Ban идёт с готовым action для AbuseIPDB. Для AbuseIPDB.RU меняется только домен в конфиге — сам action и его параметры остаются прежними.
Шаг 1. Получите ключ API
Личный кабинет: https://abuseipdb.ru/dashboard → «API-ключи» → «Создать ключ» (если раздела нет — support@abuseipdb.ru). Для Fail2Ban достаточно тарифа free (1 000 запросов в сутки): одна жалоба на один бан, при типичной нагрузке этого хватает с запасом.
Сохраните ключ в файл, доступный только root, и запомните путь:
install -m 600 /dev/null /etc/fail2ban/abuseipdb.key
printf '%s\n' 'YOUR_OWN_API_KEY' > /etc/fail2ban/abuseipdb.key
chmod 600 /etc/fail2ban/abuseipdb.key
Шаг 2. Создайте action
Файл /etc/fail2ban/action.d/abuseipdb.ru.conf:
[Definition]
# Отправляет жалобу в AbuseIPDB.RU при бане.
# Отличие от стокового abuseipdb.conf — только домен api.abuseipdb.ru.
actionban = curl --fail --silent --show-error --max-time 10 \
'https://api.abuseipdb.ru/api/v2/report' \
-H 'Accept: application/json' \
-H 'Key: <abuseipdb_apikey>' \
--data-urlencode 'comment=<matches>' \
--data-urlencode 'ip=<ip>' \
--data 'categories=<abuseipdb_category>'
[Init]
# Категории через запятую, ID 1..23. 18 = Brute-Force, 22 = SSH.
abuseipdb_category = 18,22
# Ключ читается из файла, чтобы не хранить его в открытом виде в jail.local.
# Если файла нет — подставьте ключ напрямую (и ограничьте права на конфиг).
abuseipdb_apikey = %(abuseipdb_apikey)s
Если ваш Fail2Ban не умеет читать ключ из файла через [Init], используйте обёртку:
[Definition]
actionban = /usr/local/bin/abuseipdb-report '<ip>' '<abuseipdb_category>' '<matches>'
[Init]
abuseipdb_category = 18,22
#!/usr/bin/env bash
# /usr/local/bin/abuseipdb-report — отправка одной жалобы из Fail2Ban.
set -euo pipefail
IP="$1"
CATEGORIES="${2:-18,22}"
MATCHES="${3:-banned by fail2ban}"
KEY="$(cat /etc/fail2ban/abuseipdb.key)"
curl --fail --silent --show-error --max-time 10 \
'https://api.abuseipdb.ru/api/v2/report' \
-H 'Accept: application/json' \
-H "Key: ${KEY}" \
--data-urlencode "ip=${IP}" \
--data-urlencode "comment=${MATCHES}" \
--data "categories=${CATEGORIES}"
chmod 700 /usr/local/bin/abuseipdb-report
Шаг 3. Подключите action к jail
Файл /etc/fail2ban/jail.local (или отдельный /etc/fail2ban/jail.d/sshd.local):
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 10m
bantime = 1h
# важное: action = обычный бан + отправка жалобы
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
abuseipdb.ru
Для RHEL/CentOS путь к логу — /var/log/secure, для systemd-журнала используйте backend = systemd.
Шаг 4. Перезапустите и проверьте
fail2ban-client -t # проверка конфигурации
systemctl restart fail2ban
fail2ban-client status sshd # статус jail и список забаненных
Проверка, что жалоба уходит, без реального бана:
# Ручной вызов action — отправит жалобу на публичный адрес (подставьте свой или адрес нарушителя)
abuseipdb-report 89.205.125.160 18,22 'manual test from fail2ban'
# Через API убеждаемся, что жалоба видна (ключ тот же)
curl -sG https://api.abuseipdb.ru/api/v2/check \
--data-urlencode "ipAddress=127.0.0.1" \
-d maxAgeInDays=1 -d verbose \
-H "Key: $(cat /etc/fail2ban/abuseipdb.key)" \
-H "Accept: application/json"
Если ответ 200 и в reports[] появилась ваша запись — интеграция работает. Дальше проверьте журнал Fail2Ban:
journalctl -u fail2ban -n 50 --no-pager
grep -i abuseipdb /var/log/fail2ban.log | tail
Важные нюансы
- IPv6: адрес с двоеточиями обязан быть URL-кодирован.
--data-urlencode 'ip=<ip>'в curl делает это сам; не заменяйте его на-d 'ip=<ip>'. - Комментарий:
<matches>содержит строки лога; curl кодирует их сам. Если строка лога содержит персональные данные (логин пользователя, e-mail), вырежьте их, иначе комментарий нарушит правила PII. - Категории: для SSH-брутфорса ставьте
18,22, для веб-брутфорса —18,21, для сканеров —14. Полный список — в REPORT. - Лимит: одна жалоба на бан; дедупликация 15 минут может отклонить повторную жалобу на тот же адрес. Fail2Ban при
429просто залогирует ошибку — это нормально. - Проверка ответа:
curl --failвернёт ненулевой код при4xx/5xx, и Fail2Ban запишет предупреждение в лог. Это полезно для диагностики. - Не логируйте ключ:
<abuseipdb_apikey>не должен попадать вjail.localв открытом виде, если файл читают другие пользователи.
Проверка фильтра без бана
# 1. Проверить, что фильтр ловит строки нужного формата
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 2. Проверить итоговое действие (dry-run без сети)
fail2ban-client get sshd actions
CSF: блокировка по чёрному списку
CSF (ConfigServer Security & Firewall) умеет применять список адресов из /etc/csf/csf.deny. Схема: раз в N минут скачиваем plaintext-блэклист, проверяем свежесть по X-Generated-At, затем атомарно обновляем правила.
#!/usr/bin/env bash
# /usr/local/bin/csf-abuseipdb-sync — обновление csf.deny из AbuseIPDB.RU
set -euo pipefail
KEY="$(cat /etc/fail2ban/abuseipdb.key)"
OUT=/tmp/abuseipdb.csf.deny
TMP="$(mktemp)"
curl -sS -G https://api.abuseipdb.ru/api/v2/blacklist \
-d confidenceMinimum=75 \
-d limit=10000 \
-d plaintext \
-H "Key: ${KEY}" \
-H "Accept: text/plain" \
-D /tmp/abuseipdb.headers \
-o "$TMP"
# Проверяем, что ответ непустой
test -s "$TMP" || { rm -f "$TMP"; exit 1; }
# Формируем csf.deny: # BEGIN/END-маркеры, чтобы не удалять чужие правила
{
echo '# BEGIN AbuseIPDB.RU (auto-managed, do not edit)'
grep -vE '^\s*$|^\s*#' "$TMP" | awk '{print $1 " # AbuseIPDB.RU confidence>=75"}'
echo '# END AbuseIPDB.RU'
} > "$OUT"
# Заменяем только наш блок в csf.deny
python3 - "$OUT" <<'PY'
import re
import sys
new_block = open(sys.argv[1], encoding="utf-8").read()
path = "/etc/csf/csf.deny"
try:
old = open(path, encoding="utf-8").read()
except FileNotFoundError:
old = ""
pattern = re.compile(r"# BEGIN AbuseIPDB\.RU.*?# END AbuseIPDB\.RU\n?", re.S)
old = pattern.sub("", old).rstrip() + "\n\n"
open(path, "w", encoding="utf-8").write(old + new_block)
PY
rm -f "$TMP"
csf -r >/dev/null 2>&1 || true
Добавьте в cron:
*/30 * * * * /usr/local/bin/csf-abuseipdb-sync >> /var/log/csf-abuseipdb.log 2>&1
Для CSF важно не превышать разумный размер
csf.deny: 10 000 адресов — практический потолок для большинства конфигураций. Начните сconfidenceMinimum=90иlimit=5000, измерьте нагрузкуcsf -r, затем расширяйте.
Suricata: репутация адресов в алертах
Две практичные схемы.
Схема 1. Отправлять жалобы по алертам
Скрипт читает eve.json, берёт уникальные src_ip из событий с высоким приоритетом и отправляет их в REPORT. Так ваши IDS-алерты попадают в базу без ручной работы.
#!/usr/bin/env python3
"""suricata-abuseipdb.py — жалобы из EVE JSON Suricata в AbuseIPDB.RU."""
import json
import os
import sqlite3
import time
import urllib.parse
import urllib.request
EVE = "/var/log/suricata/eve.json"
STATE = "/var/lib/suricata-abuseipdb.sqlite"
API_KEY = open("/etc/fail2ban/abuseipdb.key", encoding="utf-8").read().strip()
BASE = "https://api.abuseipdb.ru/api/v2"
# Категории: 14 = Port Scan, 21 = Web App Attack — подберите под свои правила
CATEGORIES = "14,21"
db = sqlite3.connect(STATE)
db.execute("CREATE TABLE IF NOT EXISTS sent (ip TEXT PRIMARY KEY, sent_at INTEGER)")
db.commit()
now = int(time.time())
sent = 0
with open(EVE, "r", encoding="utf-8", errors="replace") as handle:
for line in handle:
try:
event = json.loads(line)
except ValueError:
continue
if event.get("event_type") != "alert" or event.get("alert", {}).get("severity", 3) > 2:
continue
ip = event.get("src_ip", "")
if not ip:
continue
row = db.execute("SELECT sent_at FROM sent WHERE ip = ?", (ip,)).fetchone()
if row and now - row[0] < 3600: # не чаще раза в час на адрес
continue
payload = urllib.parse.urlencode(
{"ip": ip, "categories": CATEGORIES, "comment": event["alert"].get("signature", "Suricata alert")[:1024]}
).encode()
request = urllib.request.Request(
f"{BASE}/report",
data=payload,
headers={"Key": API_KEY, "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
if response.status == 200:
sent += 1
except Exception as error: # 429 дедупликации — не ошибка, пропускаем
print("report failed:", ip, error)
db.execute("INSERT OR REPLACE INTO sent (ip, sent_at) VALUES (?, ?)", (ip, now))
db.commit()
print("sent:", sent)
*/10 * * * * /usr/local/bin/suricata-abuseipdb.py >> /var/log/suricata-abuseipdb.log 2>&1
Схема 2. Обогащать алерты оценкой
При разборе eve.json запрашивайте /api/v2/check (с кэшем) и добавляйте abuseConfidenceScore в отчёт/дашборд. Это дешевле, чем гонять проверку на каждый пакет: кэшируйте ответ на 5–15 минут.
import requests
API_KEY = "YOUR_OWN_API_KEY"
_cache: dict[str, int] = {}
def score(ip: str) -> int:
if ip in _cache:
return _cache[ip]
response = requests.get(
"https://api.abuseipdb.ru/api/v2/check",
params={"ipAddress": ip},
headers={"Key": API_KEY, "Accept": "application/json"},
timeout=10,
)
response.raise_for_status()
_cache[ip] = response.json()["data"]["abuseConfidenceScore"]
return _cache[ip]
Импорт plaintext-блэклиста в
iprep/reputation-fileSuricata тоже возможен, но синтаксис репутационных файлов зависит от версии Suricata и включённой фичи. Сверьтесь с документацией вашей версии; формат plaintext, который отдаёт/api/v2/blacklist(по одному IP в строке), подходит как источник для конвертации.
Что дальше
- Лимиты запросов — чтобы Fail2Ban/скрипты не исчерпали квоту.
- Обработка ошибок — как читать
429и422в логах. - Чёрный список — параметры выгрузки и предупреждения про firewall.
Версия 2.2.0 · AbuseIPDB.RU · webmaster@abuseipdb.ru · source: docs/ABUSE-API-fail2ban.md