CrowdSec + MikroTik. Часть 5 (бонус): алерты в телефон через ntfy

CrowdSec + MikroTik. Часть 5 (бонус): алерты в телефон через ntfy

Предыдущие части: Часть 1 — локальный детект и бан на роутере, Часть 2 — community-блоклист, Часть 3 — веб-детект через Traefik, Часть 4 — Prometheus + Grafana. Здесь — как сделать так, чтобы важное само прилетало в телефон, а не ждало, пока вы откроете дашборд.

Графики из Части 4 — это прекрасно, но у них есть один изъян: в них надо смотреть. А самые неприятные вещи случаются ровно тогда, когда вы в дашборд не глядите: ночью молча умер детект (отвалился syslog-поток), CrowdSec забанил ваш же VPN-выход, роутер ушёл в ребут и потерял address-list. Хочется, чтобы про такое сообщали — пушем на телефон.

Я для этого использую ntfy — простой self-hosted сервис пуш-уведомлений: POST на топик → мгновенный пуш в приложение на телефоне. Здесь покажу, как навесить его на наш CrowdSec-конвейер.

:wrapped_gift: Это бонус-часть: схема из Частей 1–4 работает и без неё. Но один раз настроив, ловить «детект умер» вы будете за минуты, а не наутро.
Также прикладываю ссылку на GitHub-репозиторий со всем кодом из цикла — для тех, кто хочет поднять всё быстро, без копипасты из статей: GitHub - fenixvd/crowdsec-mikrotik-bouncer: Bounce CrowdSec decisions onto a MikroTik router — ban scanners & web attackers at the WAN edge via address-list (local + community + Traefik web detect) · GitHub


Есть ли у вас уже ntfy?

Да, ntfy уже крутится

Тогда из этой статьи вам нужны только URL сервера, токен и имя топика — и можно сразу прыгать в раздел «Хелпер для отправки». Свой топик под этот проект (напр. security) заведите, если хочется отделить безопасность от остальных уведомлений.

Нет, ntfy ещё нет — два пути

Путь 1. Публичный ntfy.sh (за 30 секунд, без своего сервера). Просто шлёте на https://ntfy.sh/любой-случайный-топик и подписываетесь на него в приложении. Минус один, но важный: топик = пароль. Любой, кто его знает (или угадает), читает ваши уведомления. Для алертов про безопасность лучше:

  • взять длинное случайное имя топика (security-7f3k9x2m-…), а не myhomelab;
  • не писать в текст алерта чувствительного (IP своей инфры, токены) — в публичном канале это лишнее.

Путь 2. Свой ntfy в Docker (рекомендую). Полный контроль, приватность, авторизация. Минимальный docker-compose.yml:

services:
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    command: serve
    restart: unless-stopped
    environment:
      NTFY_BASE_URL: "https://ntfy.example.com"   # или http://SERVER_IP:8090 для LAN-only
      NTFY_AUTH_DEFAULT_ACCESS: "deny-all"         # анонимам — 403, только по токену
      NTFY_BEHIND_PROXY: "true"                     # если за реверс-прокси
    volumes:
      - ./data:/var/lib/ntfy
    ports:
      - "SERVER_IP:8090:80"     # в мир — только через реверс-прокси (Traefik из Части 3)

Заводим пользователя и токен для скриптов:

docker exec -it ntfy ntfy user add --role=admin myuser
docker exec -it ntfy ntfy token add myuser     # -> tk_xxxxxxxx, его положим в скрипт

Наружу ntfy лучше выставлять через реверс-прокси (тот же Traefik + CrowdSec из Части 3), а не публиковать порт напрямую. Тогда пуши приходят из любой сети, а на edge стоит защита. Кто не хочет светить наружу вообще — держите ntfy LAN/VPN-only, пуши будут приходить дома и под VPN.


Хелпер для отправки

Чтобы не плодить длинные curl по скриптам, заведём один маленький хелпер. ntfy-notify.sh:

#!/usr/bin/env bash
# ntfy-notify.sh "Заголовок" "Текст" [приоритет] [теги]
# приоритет: min|low|default|high|urgent ; теги: warning,skull,rotating_light ...
set -euo pipefail

NTFY_URL="${NTFY_URL:-http://SERVER_IP:8090}"   # ВАЖНО: см. грабли про hairpin-NAT
NTFY_TOPIC="${NTFY_TOPIC:-security}"
NTFY_TOKEN="${NTFY_TOKEN:-tk_ВСТАВЬТЕ_ТОКЕН}"

title="${1:?title}"; body="${2:?body}"; prio="${3:-default}"; tags="${4:-}"

curl -sf -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
  -H "Authorization: Bearer ${NTFY_TOKEN}" \
  -H "Title: ${title}" \
  -H "Priority: ${prio}" \
  ${tags:+-H "Tags: ${tags}"} \
  --data-binary "${body}" >/dev/null

Секреты (NTFY_TOKEN и др.) держим не в самом скрипте, а в отдельном notify.env рядом (и в git его не коммитим / шифруем git-crypt). Проверка:

NTFY_TOKEN=tk_... ./ntfy-notify.sh "Тест" "CrowdSec на связи" default tada

Что именно слать (триггеры по нашему конвейеру)

Не «всё подряд» — иначе уведомления превратятся в шум, который вы замьютите. Слать стоит то, что значит «схема сломалась или происходит плохое». Вот мой набор по частям 1–4:

Событие Почему важно Приоритет
Поток syslog от роутера упал в 0 на N минут Детект умер (отвалился лог/ротация/роутер) — баны перестали появляться, а атаки идут urgent
Забанен IP своей инфры CrowdSec прихлопнул ваш VPN-выход/CDN/пир — вы сами себе отрубили доступ urgent
Резкий всплеск новых банов Идёт активная атака/скан — полезно знать, что началось high
Роутер недоступен по API Баунсер не может разложить баны — address-list не обновляется high
Память CrowdSec выше порога На слабой машинке после community-листа можно словить OOM default
CrowdSec/баунсер контейнер упал Очевидно urgent

Ниже — две стратегии доставки. Выбирайте по тому, что у вас уже есть.


Стратегия A: скрипт по таймеру (без Alertmanager)

Если городить Prometheus Alertmanager не хочется — простой скрипт + systemd-таймер. Он спрашивает cscli/PromQL и дёргает наш хелпер. Пример проверки «детект жив» и «не забанили ли своих»:

#!/usr/bin/env bash
# crowdsec-watch.sh — гоняется systemd-таймером раз в ~5 мин
set -euo pipefail
NOTIFY=/path/to/ntfy-notify.sh
STATE=/var/lib/crowdsec-watch ; mkdir -p "$STATE"

# 1) Детект жив? Спрашиваем Prometheus: были ли строки syslog за 10 минут.
#    (если Prometheus нет — можно считать строки в /var/log/mikrotik.log за период)
q='sum(increase(cs_syslogsource_hits_total[10m]))'
hits=$(curl -sf "http://SERVER_IP:9090/api/v1/query?query=${q}" \
        | grep -oE '"value":\[[0-9.]+,"[0-9.]+"' | grep -oE '"[0-9.]+"$' | tr -d '"' || echo 0)
if awk "BEGIN{exit !($hits==0)}"; then
  # шлём только при СМЕНЕ состояния (см. дедуп ниже)
  [ -f "$STATE/syslog_dead" ] || "$NOTIFY" "CrowdSec: детект умер" \
     "Нет строк syslog от роутера за 10 мин. Проверь rsyslog/ротацию/роутер." urgent rotating_light
  touch "$STATE/syslog_dead"
else
  [ -f "$STATE/syslog_dead" ] && "$NOTIFY" "CrowdSec: детект восстановлен" "syslog снова идёт" default white_check_mark
  rm -f "$STATE/syslog_dead"
fi

# 2) Не забанили ли СВОИХ? known-IP берём динамически из вайтлиста (Часть 1, грабли №3)
known=$(grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' /path/to/cfg/parsers/s02-enrich/my-whitelist.yaml || true)
banned=$(docker exec crowdsec cscli decisions list -o json --limit 0 \
         | grep -oE '"value":"[0-9.]+"' | grep -oE '[0-9.]+' || true)
for ip in $known; do
  if grep -qx "$ip" <<<"$banned"; then
    "$NOTIFY" "CrowdSec: самобан!" "Своя инфра в бане: $ip — снимай: cscli decisions delete --ip $ip" urgent skull
  fi
done

systemd-таймер:

# /etc/systemd/system/crowdsec-watch.timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target

И ещё один бесплатный приём: уведомление при падении любого юнита через OnFailure. Вешаете на CrowdSec/баунсер/таймеры drop-in:

# ~/.config/systemd/.../override или системный drop-in
[Unit]
OnFailure=ntfy-failure@%n.service

где ntfy-failure@.service просто зовёт хелпер с именем упавшего юнита (%i). Тогда любой краш сам стучится в телефон без отдельной логики.


Стратегия B: через Grafana (если она у вас из Части 4)

Подняли Grafana — у неё есть встроенный Alerting, и ntfy подключается как contact point типа Webhook. Это удобно: правила живут рядом с графиками.

  1. Contact point (Alerting → Contact points → Add):

    • Type: Webhook
    • URL: https://ntfy.example.com/security
    • HTTP Method: POST
    • В заголовки добавить Authorization: Bearer tk_... (если ntfy под авторизацией).

    ntfy умеет принимать и Grafana-вебхук «как есть», но красивее — лёгкий промежуточный скрипт/функция, которая разворачивает payload в Title/Priority/теги ntfy. Минимально достаточно слать тело — пуш придёт.

  2. Alert rule — например на тот же «детект умер»:

    • Query: sum(increase(cs_syslogsource_hits_total[10m]))
    • Condition: IS BELOW 1
    • For: 10m
    • Notifications → ваш ntfy contact point.

Аналогично заводятся правила на cs_active_decisions (всплеск), process_resident_memory_bytes{job="crowdsec"} (память), отсутствие таргета (up{job="crowdsec"} == 0).

Что выбрать? Нет Grafana или хотите минимум зависимостей — Стратегия A (скрипт + таймер). Уже живёте в Grafana и любите правила в UI — Стратегия B. Я у себя совмещаю: критичное про инфру шлёт самописный монитор по таймеру (стратегия A — он же проверяет вещи вне Prometheus), а пороговые графики — Grafana.


Грабли и заметки

  1. Hairpin-NAT: с самого сервера шлите по ЛОКАЛЬНОМУ адресу. Классическая засада: ntfy наружу живёт на https://ntfy.example.com, но скрипт на том же сервере, дёргая этот FQDN, упирается в собственный WAN — а NAT-hairpin на роутере часто выключен, и пуш молча не уходит. Решение: в notify.env для серверных скриптов укажите внутренний адрес (http://SERVER_IP:8090). Токен работает и по HTTP в LAN.

  2. Шлите только при СМЕНЕ состояния + «восстановлено». Если слать алерт каждые 5 минут, пока проблема жива, — вы его замьютите и пропустите следующий. Храните флажок в /var/lib/... и пушите один раз на вход в проблему и один раз на выход (как в скрипте выше).

  3. Приоритеты и теги — не для красоты. urgent на Android пробивает Do-Not-Disturb, min приходит тихо. Самобан своей инфры и смерть детекта — urgent; «началась атака» — high; рутина — default/low. Теги (rotating_light, skull, white_check_mark) превращаются в эмодзи — мгновенно видно глазами, что прилетело.

    Оговорюсь: всё про приоритеты я проверял на Android. iPhone у меня нет, так что как там ведут себя приоритеты/прохождение через «Не беспокоить» — не подскажу; на iOS доставка идёт через APNs и поведение может отличаться. Если кто пользуется ntfy на айфоне — поделитесь в комментариях, как оно.

  4. Не кладите токен в скрипт/в git. Только notify.env рядом, зашифрованный (git-crypt/SOPS) или просто в .gitignore. В публичный топик ntfy.sh не пишите IP своей инфры.

  5. Проверьте доставку «вживую». Один раз искусственно уроните условие (остановите rsyslog на минуту) и убедитесь, что пуш дошёл. Молчащий алертинг хуже отсутствующего — он создаёт ложное чувство защищённости.


Вместо эпилога

Вот и всё. За пять частей мы прошли путь от одной строчки в логе файрвола до системы, которая сама ловит, банит и докладывает:

лог на краю сети  →  детект  →  бан на роутере  →  глобальная репутация
        →  веб-атаки  →  графики  →  пуш в телефон

Красота ведь в том, что хорошая защита — это тишина.

Но самое ценное — это не конкретные конфиги, а подход. CrowdSec детектит, баунсер исполняет, роутер режет, ntfy будит — каждый кубик заменяем и до-страиваем. Захотите добавить новый источник логов, ещё один список, свой сценарий, второй роутер, алерт в Telegram вместо ntfy — каркас выдержит. Это не готовая «коробка», которую страшно трогать, а конструктор, который растёт вместе с вами. Так что дальше — только ваша фантазия и руки)

Спасибо, что дочитали весь цикл до конца. Поднимайте, ломайте, переделывайте под себя. Вопросы, замечания — как всегда, жду в комментариях.

Спасибо за подробную статью! Я тоже через ntfy забираю алерты из Grafana — самое то для домашнего сервера, где нет круглосуточного дежурного. С вебхуками дружит из коробки, настройка контакт-поинта занимает минуту.

Интересно, кто-то пробовал гонять через ntfy не только критические алерты, но и ежедневные отчёты — состояние zpool, нагрузку CPU, лог ротацию? У меня пока только важное летит, но думаю расширить. Не превратится ли в спам?

Если использовать в уведомлениях state файлик как я писал в статье то не должно спамить, по крайней мере у меня не спамит, присылает исключительно то что меняет свой статус)