Тема
Войти Регистрация

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-file Suricata тоже возможен, но синтаксис репутационных файлов зависит от версии Suricata и включённой фичи. Сверьтесь с документацией вашей версии; формат plaintext, который отдаёт /api/v2/blacklist (по одному IP в строке), подходит как источник для конвертации.

Что дальше

Версия 2.2.0 · AbuseIPDB.RU · webmaster@abuseipdb.ru · source: docs/ABUSE-API-fail2ban.md