CrowdSec + MikroTik. Часть 5 (бонус): алерты в телефон через ntfy
Предыдущие части: Часть 1 — локальный детект и бан на роутере, Часть 2 — community-блоклист, Часть 3 — веб-детект через Traefik, Часть 4 — Prometheus + Grafana. Здесь — как сделать так, чтобы важное само прилетало в телефон, а не ждало, пока вы откроете дашборд.
Графики из Части 4 — это прекрасно, но у них есть один изъян: в них надо смотреть. А самые неприятные вещи случаются ровно тогда, когда вы в дашборд не глядите: ночью молча умер детект (отвалился syslog-поток), CrowdSec забанил ваш же VPN-выход, роутер ушёл в ребут и потерял address-list. Хочется, чтобы про такое сообщали — пушем на телефон.
Я для этого использую ntfy — простой self-hosted сервис пуш-уведомлений: POST на топик → мгновенный пуш в приложение на телефоне. Здесь покажу, как навесить его на наш CrowdSec-конвейер.
Это бонус-часть: схема из Частей 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. Это удобно: правила живут рядом с графиками.
-
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. Минимально достаточно слать тело — пуш придёт. -
Alert rule — например на тот же «детект умер»:
- Query:
sum(increase(cs_syslogsource_hits_total[10m])) - Condition:
IS BELOW 1 - For:
10m - Notifications → ваш ntfy contact point.
- Query:
Аналогично заводятся правила на cs_active_decisions (всплеск), process_resident_memory_bytes{job="crowdsec"} (память), отсутствие таргета (up{job="crowdsec"} == 0).
Что выбрать? Нет Grafana или хотите минимум зависимостей — Стратегия A (скрипт + таймер). Уже живёте в Grafana и любите правила в UI — Стратегия B. Я у себя совмещаю: критичное про инфру шлёт самописный монитор по таймеру (стратегия A — он же проверяет вещи вне Prometheus), а пороговые графики — Grafana.
Грабли и заметки
-
Hairpin-NAT: с самого сервера шлите по ЛОКАЛЬНОМУ адресу. Классическая засада: ntfy наружу живёт на
https://ntfy.example.com, но скрипт на том же сервере, дёргая этот FQDN, упирается в собственный WAN — а NAT-hairpin на роутере часто выключен, и пуш молча не уходит. Решение: вnotify.envдля серверных скриптов укажите внутренний адрес (http://SERVER_IP:8090). Токен работает и по HTTP в LAN. -
Шлите только при СМЕНЕ состояния + «восстановлено». Если слать алерт каждые 5 минут, пока проблема жива, — вы его замьютите и пропустите следующий. Храните флажок в
/var/lib/...и пушите один раз на вход в проблему и один раз на выход (как в скрипте выше). -
Приоритеты и теги — не для красоты.
urgentна Android пробивает Do-Not-Disturb,minприходит тихо. Самобан своей инфры и смерть детекта —urgent; «началась атака» —high; рутина —default/low. Теги (rotating_light,skull,white_check_mark) превращаются в эмодзи — мгновенно видно глазами, что прилетело.Оговорюсь: всё про приоритеты я проверял на Android. iPhone у меня нет, так что как там ведут себя приоритеты/прохождение через «Не беспокоить» — не подскажу; на iOS доставка идёт через APNs и поведение может отличаться. Если кто пользуется ntfy на айфоне — поделитесь в комментариях, как оно.
-
Не кладите токен в скрипт/в git. Только
notify.envрядом, зашифрованный (git-crypt/SOPS) или просто в.gitignore. В публичный топик ntfy.sh не пишите IP своей инфры. -
Проверьте доставку «вживую». Один раз искусственно уроните условие (остановите rsyslog на минуту) и убедитесь, что пуш дошёл. Молчащий алертинг хуже отсутствующего — он создаёт ложное чувство защищённости.
Вместо эпилога
Вот и всё. За пять частей мы прошли путь от одной строчки в логе файрвола до системы, которая сама ловит, банит и докладывает:
лог на краю сети → детект → бан на роутере → глобальная репутация
→ веб-атаки → графики → пуш в телефон
Красота ведь в том, что хорошая защита — это тишина.
Но самое ценное — это не конкретные конфиги, а подход. CrowdSec детектит, баунсер исполняет, роутер режет, ntfy будит — каждый кубик заменяем и до-страиваем. Захотите добавить новый источник логов, ещё один список, свой сценарий, второй роутер, алерт в Telegram вместо ntfy — каркас выдержит. Это не готовая «коробка», которую страшно трогать, а конструктор, который растёт вместе с вами. Так что дальше — только ваша фантазия и руки)
Спасибо, что дочитали весь цикл до конца. Поднимайте, ломайте, переделывайте под себя. Вопросы, замечания — как всегда, жду в комментариях.