CrowdSec на Debian 13: пять граблей, которых нет в туториалах
Короткий ответ: CrowdSec на Debian 13 (trixie) ставится за вечер, но три вещи ведут себя не так, как в гайдах: OpenSSH 9.8 пишет логи от имени sshd-session, а не sshd; ssh-логи надо забирать из journald, потому что /var/log/auth.log больше не главный источник; бан-таблица nftables создаётся в семействе ip, а не inet — и её легко «не найти». Плюс два организационных правила: whitelist со своими IP — до первого запуска, на боевой прод — сначала режим detect-only без банов.
На хостах у меня много лет жил самодельный слой защиты: nginx-конфиг с блокировкой по User-Agent и крон, который раз в сутки тянул список Tor-exit-нод и вбивал его в nginx через deny. При переезде на Debian 13 я наконец разобрался, чего этот слой стоит, выкинул его и поставил CrowdSec с nftables-bouncer’ом. По дороге собрал пять граблей — ни одной из них нет в туториалах.
Коротко (TL;DR)
- Решение: CrowdSec (агент + nftables firewall-bouncer) с acquisition через journald. Bouncer держит собственную nft-таблицу и мирно живёт рядом с уже существующим кастомным firewall.
- Альтернатива: fail2ban — если нужен только ssh и не нужны краудсорс-блоклисты. Но с
sshd-sessionиз OpenSSH 9.8 наивные regex-фильтры сломаются точно так же. - Грабли:
cscli metricsпоказывает ssh-логи как unparsed (это норма); таблица бана —nft list table ip crowdsec, неinet; смена порта вsshd_configигнорируется при socket-активации; без whitelist забанишь свой же IP; на прод с клиентами — сначала detect-only.
Почему крон с Tor-блоклистом — не защита
Ежедневная выгрузка Tor-нод в nginx deny — ритуал, а не защита, и держать его в 2026 году нет смысла. Разбор моего крона показал три проблемы. Во-первых, он закрывает только Tor, а реальные атаки приходят с VPN, облачных провайдеров и ботнетов — по ним списка нет. Во-вторых, после обновления списка крон делал nginx restart вместо reload — раз в сутки рвались все живые соединения. В-третьих, гигантский deny-список nginx проверяет линейно на каждый запрос.
То есть слой создавал видимость работы: что-то качается, что-то блокируется, в логах движение. А фактическую атаку — перебор паролей ssh, сканирование nginx — он не видел вовсе.
CrowdSec или fail2ban
Я выбрал CrowdSec: он закрывает те же сценарии, что fail2ban, плюс даёт краудсорс-репутацию IP, а риск «вендор умрёт» снимается лицензией и локальной архитектурой. Сравнение по пунктам, которые важны были мне:
| fail2ban | CrowdSec | |
|---|---|---|
| Механизм детекта | regex по строкам лога | парсеры + сценарии (leaky bucket), многострочная логика |
| Источники логов | файлы | файлы, journald, docker и другие |
| Механизм бана | сам пишет правила в iptables/nftables | отдельный bouncer со своей nft-таблицей |
| Репутация сообщества | нет | краудсорс-блоклисты (обмен сигналами) |
| Работа без вендора | полная автономия | полная: ядро под MIT, решения принимает локальный LAPI |
Последняя строка была для меня решающей. Был страх: CrowdSec — компания, а вдруг она закроется. Проверил: ядро CrowdSec распространяется под MIT, детект и баны работают полностью локально через Local API. Если компания исчезнет, потеряется только краудсорс-репутация — сам IPS продолжит работать.
Как поставить: агент, коллекции, bouncer
Порядок такой: сначала агент с коллекциями и whitelist’ом, обкатка без банов, и только потом bouncer.
- Подключить репозиторий CrowdSec и поставить агент:
apt install crowdsec. - Поставить коллекции под свой стек:
cscli collections install crowdsecurity/sshd crowdsecurity/nginx crowdsecurity/linux. - Настроить acquisition: ssh — через journald, nginx — по glob лог-файлов (ниже).
- Добавить whitelist со своими IP и внутренними сетями (ниже, шаг обязательный).
- Перезапустить агент и проверить
cscli metrics— логи должны читаться. - Обкатать detect-only: агент детектит и пишет решения в
cscli decisions list, но банить некому. - Когда ложных срабатываний нет — поставить bouncer:
apt install crowdsec-firewall-bouncer-nftables.
Acquisition для ssh на Debian 13 надёжнее делать через journald, а не через файл (документация CrowdSec по journald-источнику):
| |
Грабли 1: cscli metrics показывает ssh-логи как unparsed
Строки «unparsed» в cscli metrics по ssh — не признак поломки: парсер намеренно не разбирает записи об успешных логинах. Я на это попался: увидел ssh-логи в колонке unparsed и решил, что парсер не работает на свежем стеке.
Причин запутаться сразу две. Первая — в release notes OpenSSH 9.8 сказано прямо: «the server has been split into a listener binary, sshd(8), and a per-session binary “sshd-session”». То есть строки о сессиях в логе теперь идут от процесса sshd-session, и любой фильтр, который ждёт sshd[, молча перестаёт находить их. Вторая — на Debian 13 auth-события живут в journald, и привычки смотреть /var/log/auth.log больше недостаточно.
Проверить, что детект жив, можно за минуту — скормить парсеру строку неудачного логина:
| |
В выводе видно всю цепочку: строка парсится и попадает во все ssh-bruteforce-сценарии. Детект работал с самого начала — вводила в заблуждение метрика.
Грабли 2: nft list table inet crowdsec пустой
Bouncer создаёт таблицы в семействах ip и ip6, а не inet — проверять бан надо командой nft list table ip crowdsec. Я после первого бана полез в inet, увидел пустоту и решил, что баны не работают.
Документация firewall-bouncer это подтверждает: для IPv4 создаётся table ip crowdsec с сетом crowdsec-blacklists, для IPv6 — table ip6 crowdsec6. Рабочие команды диагностики:
| |
Отдельная таблица — это и ответ на вопрос «не подерётся ли bouncer с моим кастомным firewall». На моих нодах уже был свой набор nftables-правил, и я боялся, что новый инструмент его сломает. Не подрался: у bouncer’а своя таблица, оба хука input отрабатывают независимо, и DROP в любой из таблиц финален. Существующие правила я не трогал вообще.
Грабли 3: смена порта в sshd_config не работает
Если ssh запущен через socket-активацию systemd, параметр Port в sshd_config игнорируется — слушающий сокет описан в юните ssh.socket, и менять порт надо там либо возвращать классический service-режим. Я в рамках той же чистки решил увести ssh со стандартного порта, поправил конфиг, сделал рестарт — порт не изменился.
Диагноз ставится одной командой: systemctl status ssh.socket. Если сокет активен — правки Port в конфиге бесполезны. Я вернул службу в обычный режим:
| |
После этого sshd_config снова источник правды. Прямого отношения к CrowdSec эти грабли не имеют, но наступил я на них в той же сессии, и в паре с «логи теперь от sshd-session» они складываются в общий вывод: на свежем Debian старые привычки вокруг ssh стоит перепроверять по одной.
Почему whitelist нужен до первого запуска
Без whitelist CrowdSec забанит ваш собственный IP при первой же проверке типа «а задетектит ли он брутфорс» — и вы останетесь без доступа к хосту. Документация CrowdSec определяет их так: «Whitelists are special parsers that allow you to “discard” events» — событие отбрасывается на этапе парсинга и до сценариев не доходит.
Минимальный parser-whitelist:
| |
Меня этот файл однажды спас. На одном из хостов я забыл про план «сначала без банов» и поставил bouncer сразу — CrowdSec встал в боевой режим на работающем проде. Обошлось: whitelist не дал системе залочить меня самого, а забанены оказались только реальные сканеры. Но полагаться на такое везение на хосте с клиентским трафиком нельзя.
Как раскатывать на прод: сначала detect-only
На критичный прод CrowdSec ставится в два этапа: сначала агент без bouncer’а — детект есть, банов нет; и только после недели чистых логов — bouncer с реальными банами. «Режим» здесь — просто отсутствие пакета crowdsec-firewall-bouncer-nftables: агент пишет решения в cscli decisions list, а исполнять их некому.
Логика простая. Ложный бан на внутреннем хосте — неприятность для админа. Ложный бан на edge-ноде — заблокированный клиент, то есть деньги. Поэтому раскатку по парку я в Ansible-роли развёл переключателем: некритичные хосты получают агент и bouncer сразу, боевые — сначала только агент. Список хостов под IPS — явным перечнем в плейбуке: попытка вывести «умные» группы под каждый инструмент — лишняя инженерия, которая на десятке хостов ничего не даёт.
Итог
Если ставите CrowdSec на Debian 13 — закладывайте вечер и держите под рукой три команды: cscli explain (проверить, что детект жив, не доверяя колонке unparsed), nft list table ip crowdsec (смотреть баны в правильном семействе) и systemctl status ssh.socket (понять, кто на самом деле слушает порт). Whitelist со своими IP — до первого запуска, bouncer на боевом проде — только после обкатки без банов. А самодельные блоклисты типа «крон с Tor-нодами» спокойно выкидывайте: после разбора оказалось, что мой не защищал ничего — только рестартовал nginx раз в сутки.
<< Previous Post
|
Next Post >>