CrowdSec + MikroTik. Часть 4: наблюдаемость — Prometheus + Grafana
Предыдущие части: Часть 1 — локальный детект и бан на роутере, Часть 2 — community-блоклист, Часть 3 — веб-детект через Traefik. Здесь — как смотреть на всё это глазами, а не
cscli-командами: что CrowdSec отдаёт в Prometheus, какие метрики что значат, и как собрать дашборды в Grafana.
Когда конвейер из прошлых частей заработал, начинается следующий вопрос: а как оно себя чувствует? Сколько решений активно прямо сейчас, какой сценарий чаще всего срабатывает, не залип ли парсер, не «голодает» ли быстрый локальный цикл, хватает ли серверу памяти под всё это хозяйство. Гонять cscli metrics руками — рабочий вариант, но это разовый снимок. Хочется график во времени.
CrowdSec из коробки умеет отдавать метрики в формате Prometheus — остаётся их собрать и нарисовать.
Стек: Prometheus, Grafana, node-exporter (метрики хоста), cadvisor (метрики контейнеров). Всё в Docker, рядом с CrowdSec из прошлых частей.
Архитектура сбора
Ничего экзотического — классический pull: Prometheus сам ходит по эндпоинтам /metrics и складывает к себе, Grafana рисует.
scrape /metrics (pull, раз в 30с)
┌──────────────┐ ◄───────────────┐
│ CrowdSec │ :6060 │
│ (LAPI+агент)│ │
└──────────────┘ │
┌──────────────┐ ┌────────────────┐ ┌──────────┐
│ node-exporter│ :9100 ◄─┤ Prometheus │ ◄─── │ Grafana │
│ (хост) │ │ (TSDB :9090) │ PromQL│ (:3001) │
└──────────────┘ └────────────────┘ └──────────┘
┌──────────────┐ ▲
│ cadvisor │ :8080 ───────────┘
│ (контейнеры) │
└──────────────┘
Важная деталь: баунсер (наш питоновский скрипт из Части 1/2) метрики НЕ отдаёт — он самописный и неинструментирован. Поэтому «сколько IP сейчас в address-list на самом роутере» Prometheus напрямую не видит. Но он видит то, что CrowdSec хочет забанить (cs_active_decisions) — а это, при живом баунсере, и есть то, что лежит на роутере. Как дотянуться до самого MikroTik — в конце, в «Куда расти».
Шаг 1. Включаем метрики в CrowdSec
CrowdSec уже умеет в Prometheus — надо только включить эндпоинт. В cfg/config.yaml:
prometheus:
enabled: true
level: full # full = по-объектная разбивка (по сценариям, парсерам, источникам)
listen_addr: 0.0.0.0 # внутри docker-сети; наружу порт НЕ публикуем
listen_port: 6060
level: full — ключевое. На aggregated вы получите только суммарные счётчики, а на full — разбивку по именам сценариев, парсеров, источников (именно это и делает дашборды полезными: видно, какой сценарий сработал и какой парсер отвалился).
Порт 6060 наружу не пробрасываем — его дёргает только Prometheus внутри docker-сети. После правки —
docker restart crowdsecи проверка:docker exec crowdsec wget -qO- localhost:6060/metrics | head.
Шаг 2. Prometheus: кого опрашивать
Уже держите свой Prometheus + Grafana? Тогда этот шаг (и весь стек ниже —
prometheus/grafana/node-exporter/cadvisor) разворачивать заново не нужно: вам хватит одного scrape-job в существующий конфиг. Прыгайте сразу в Шаг 2-бис. Этот шаг — для тех, кто поднимает мониторинг с нуля.
prometheus/prometheus.yml:
global:
scrape_interval: 30s
scrape_timeout: 15s
scrape_configs:
- job_name: crowdsec
static_configs:
- targets: ['crowdsec:6060'] # по имени контейнера в общей docker-сети
- job_name: node # метрики хоста
static_configs:
- targets: ['SERVER_IP:9100']
- job_name: cadvisor # метрики по контейнерам
static_configs:
- targets: ['cadvisor:8080']
scrape_interval: 30s для домашнего стека более чем достаточно — сканеры это не секундные всплески, а график на 30-секундном шаге читается отлично и не раздувает TSDB.
cadvisor прожорлив. Он сам по себе заметно ест CPU и память (легко сотни мегабайт RSS, плюс постоянная нагрузка на CPU от обхода cgroups) — на слабой виртуалке/мини-ПК это ощутимо. Если ресурсы ограничены, cadvisor не обязателен: оставьте node-exporter (он лёгкий и даёт картину по хосту целиком), а разбивку по контейнерам при нужде смотрите разово через
docker stats. Хотите оставить, но прижать аппетит — поможет, например,--housekeeping_interval=30sи--docker_only=true, плюс лимиты памяти на сам контейнер.
Сервисы в docker-compose.yml (рядом с crowdsec):
prometheus:
image: prom/prometheus:latest
container_name: crowdsec-prometheus
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prom-data:/prometheus
# порт 9090 наружу не публикуем — Grafana ходит к нему по docker-сети
grafana:
image: grafana/grafana:latest
container_name: crowdsec-grafana
restart: unless-stopped
depends_on: [prometheus]
ports:
- "3001:3000" # вебморда Grafana
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
network_mode: host
pid: host
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true
devices: [ /dev/kmsg ]
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
volumes:
prom-data:
grafana-data:
Сам Prometheus наружу тоже можно не публиковать (порт 9090) — Grafana и так достучится к нему по имени
prometheus:9090внутри docker-сети. Наружу торчит только Grafana. Меньше открытых портов — спокойнее спится
Шаг 2-бис. У вас УЖЕ есть Prometheus + Grafana?
Если вы дочитали до этой части, велик шанс, что мониторинг у вас давно поднят — и разворачивать рядом второй Prometheus/Grafana (а заодно ещё одни node-exporter/cadvisor) вам совершенно не нужно. Тогда из всего шага 2 вам нужно ровно одно: добавить CrowdSec как ещё одну цель (target) в существующий Prometheus. Дальше — три типичных расклада по сети.
Расклад А: ваш Prometheus и CrowdSec в одной docker-сети
Самый простой случай. Просто дописываем job в свой prometheus.yml — и всё, ходим по имени контейнера:
- job_name: crowdsec
static_configs:
- targets: ['crowdsec:6060']
labels:
instance: 'home-crowdsec' # чтобы не путать, если инстансов будет несколько
docker exec <prometheus> kill -HUP 1 (или перезапуск) — и цель появится в Status → Targets. Порт наружу публиковать не надо.
Расклад Б: Prometheus в другой docker-сети на том же хосте
CrowdSec и ваш Prometheus в разных compose-проектах → по имени контейнера друг друга не видят. Два варианта:
-
Подключить контейнеры к общей сети (без публикации портов наружу — чище всего):
docker network connect <сеть_прометея> crowdsecПосле этого Prometheus достучится до
crowdsec:6060. -
Опубликовать 6060 на localhost и скрапить через адрес docker-моста:
# в compose с crowdsec ports: - "127.0.0.1:6060:6060" # только на петлю, не в мир!# в вашем prometheus.yml (172.17.0.1 — стандартный docker0-gateway) - job_name: crowdsec static_configs: - targets: ['172.17.0.1:6060']
Расклад В: Prometheus на ДРУГОМ хосте
Метрики надо вытащить за пределы машины с CrowdSec. Эндпоинт /metrics — это голый HTTP без авторизации, поэтому в мир его выставлять нельзя. Варианты по убыванию аккуратности:
-
Скрапить через VPN/приватную сеть (WireGuard/ZeroTier и т.п.) — Prometheus ходит на внутренний адрес. Лучший вариант.
-
Открыть 6060 только на LAN-интерфейс и зафайрволить по IP Prometheus:
ports: - "SERVER_IP:6060:6060" # только LAN, + правило файрвола на src=IP_прометея -
Совсем не хочется городить — можно поставить на хост с CrowdSec лёгкий
prometheus-agent(remote-write в центральный Prometheus), но для дома это обычно перебор.
Правило простое: 6060 не должен торчать в интернет никогда. Это незащищённый эндпоинт, который выдаёт всю внутреннюю кухню. Только loopback / docker-сеть / VPN / зафайрволенный LAN.
Grafana вы тоже уже имеете
Тогда provisioning из шага 5 не нужен — просто импортните готовые дашборды в свою Grafana (Dashboards → Import):
- ID 13927 — «CrowdSec» (overview по решениям/сценариям/парсерам);
- метрики хоста/контейнеров у вас наверняка уже рисуются вашими node-exporter/cadvisor — отдельный CrowdSec-дашборд просто встанет рядом.
При импорте выберите свой Prometheus-датасорс. Если инстансов CrowdSec несколько — пригодится переменная дашборда instance (по тому самому лейблу из job выше).
Итого для «у меня всё есть»: один job в
prometheus.yml+ один импортнутый дашборд. Контейнерыprometheus,grafana,node-exporter,cadvisorиз шага 2 вам не нужны — пропускайте их.
Шаг 3. Какие метрики отдаёт CrowdSec и что они значат
Самое интересное. CrowdSec отдаёт весь конвейер из прошлых частей, разбитый по стадиям. Префикс у всех — cs_. Ниже — те, ради которых всё и затевалось.
Поток на входе (acquisition) — сколько строк лога прочитали
| Метрика | Что отдаёт |
|---|---|
cs_syslogsource_hits_total |
строки, прилетевшие по syslog (наш поток от MikroTik из Части 1) |
cs_filesource_hits_total |
строки, прочитанные из файлов-логов (напр. accesslog Traefik из Части 3) |
Если эти счётчики не растут — CrowdSec не видит логи (привет, грабли №1 из Части 1 про монтирование каталога). Это первое, на что смотрю.
Парсинг — распарсилось или нет
| Метрика | Что отдаёт |
|---|---|
cs_parser_hits_ok_total |
строки, которые парсер успешно разобрал (по имени парсера) |
cs_parser_hits_ko_total |
строки, которые парсер не смог разобрать |
cs_node_hits_ok_total / cs_node_hits_ko_total |
то же, но по узлам парсера (грок-нодам) |
Растёт ko, а ok стоит — значит ваш грок-паттерн не матчит формат лога (поменялся формат строки MikroTik, например). Очень наглядно видно «починил парсер / сломал парсер».
Сценарии и «вёдра» (leaky buckets) — кто и как часто переполняется
| Метрика | Что отдаёт |
|---|---|
cs_buckets |
сколько вёдер сейчас «живых» (по имени сценария) — т.е. сколько IP прямо сейчас под наблюдением |
cs_bucket_created_total |
сколько вёдер создано всего (новый подозрительный IP) |
cs_bucket_overflowed_total |
сколько вёдер переполнилось → решение о бане (по имени сценария) |
cs_bucket_underflowed_total |
вёдра, «подтёкшие» в ноль без бана (трафик прекратился — ложная тревога рассосалась) |
cs_bucket_overflowed_total по сценарию custom/mikrotik-port-scan — это, по сути, пульс всей схемы: каждый прирост = пойманный сканер.
Решения и алерты — что в итоге банится
| Метрика | Что отдаёт |
|---|---|
cs_active_decisions |
активные решения прямо сейчас — с разбивкой by (action) (ban/captcha) и by (reason) (по какому сценарию) и by (origin) (свой детект / community / CAPI) |
cs_alerts |
сработавшие алерты |
cs_info |
служебная — версия/инстанс (удобно для count(cs_info) = сколько нод живо) |
sum(cs_active_decisions) by (origin) сразу показывает баланс: сколько банов дал ваш локальный детект, а сколько притащил community-блоклист из Части 2.
LAPI и производительность — не залипает ли движок
| Метрика | Что отдаёт |
|---|---|
cs_lapi_request_duration_seconds |
латентность запросов к LAPI (гистограмма по эндпоинтам) — сюда же стучится баунсер за /v1/decisions/stream |
cs_parsing_time_seconds |
сколько времени уходит на парсинг |
cs_bucket_pour_seconds |
сколько времени уходит на «разлив» событий по вёдрам |
process_resident_memory_bytes, rate(process_cpu_seconds_total[5m]), process_open_fds |
стандартные process-метрики самого CrowdSec — память, CPU, открытые файлы |
На слабой машинке process_resident_memory_bytes по CrowdSec — то, за чем стоит приглядывать после подключения большого community-листа.
Шаг 4. node-exporter и cadvisor — здоровье железа и контейнеров
CrowdSec-метрики говорят про детект, но не про то, не задыхается ли сам сервер. Это закрывают два экспортёра:
- node-exporter (
:9100) — хост целиком: CPU (node_cpu_seconds_total), память (node_memory_MemAvailable_bytes), диск (node_filesystem_avail_bytes), сеть (node_network_receive_bytes_total), load average. Запущен сnetwork_mode: hostиpid: host, чтобы видеть реальные метрики хоста, а не контейнера. - cadvisor (
:8080) — разбивка по каждому контейнеру:container_memory_usage_bytes,container_cpu_usage_seconds_total, сеть и I/O по имени контейнера. Удобно ловить, кто из контейнеров течёт по памяти.
Шаг 5. Grafana как код (provisioning)
Чтобы не кликать датасорсы и дашборды руками (и чтобы всё лежало в git), Grafana настраивается через provisioning — YAML, которые она читает при старте.
grafana/provisioning/datasources/ds.yml — подключаем Prometheus автоматически:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090 # по имени контейнера в docker-сети
isDefault: true
grafana/provisioning/dashboards/dash.yml — откуда брать json-дашборды:
apiVersion: 1
providers:
- name: dashboards
folder: ''
type: file
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: true # подпапки -> папки в Grafana
foldersFromFilesStructure: true — приятная мелочь: раскладываете json по подкаталогам, и в Grafana они становятся папками. У меня вышло так:
grafana/dashboards/
├── overview-home.json # стартовый дашборд (GF_DASHBOARDS_DEFAULT_HOME...)
├── crowdsec/ # overview / details / insight / lapi
├── host/ # node-exporter: CPU/RAM/диск/сеть
├── containers/ # cadvisor: по контейнерам
└── apps/ # прочие сервисы
Дашборды по CrowdSec не обязательно рисовать с нуля — на grafana.com есть готовые (ID 13927 «CrowdSec Overview» и компаньоны), их можно импортнуть и подогнать. Часть панелей у меня оттуда, часть — свои.
Шаг 6. Полезные панели (готовый PromQL)
Несколько запросов, которые реально висят у меня и отвечают на «ну как там оно»:
# Сколько решений активно прямо сейчас, по типу действия
sum(cs_active_decisions) by (action)
# Баланс: свой детект vs community-блоклист (origin)
sum(cs_active_decisions) by (origin)
# Топ сценариев по причине бана
sum(cs_active_decisions) by (reason)
# Пульс детекта: переполнения вёдер (= новые баны) по сценариям
sum(increase(cs_bucket_overflowed_total[$__interval])) by (name)
# Сколько IP прямо сейчас "под наблюдением" (живые вёдра) по сценариям
sum(cs_buckets) by (name)
# Здоровье парсинга: не растёт ли доля нераспарсенного
sum(increase(cs_parser_hits_ko_total[$__interval])) by (name)
# Поток логов на входе (syslog от роутера живой?)
sum(increase(cs_syslogsource_hits_total[$__interval])) by (instance)
# Память самого CrowdSec
process_resident_memory_bytes{job="crowdsec"}
$__interval — встроенная переменная Grafana (шаг панели), не надо хардкодить окно.
Грабли и заметки
level: fullобязателен для осмысленных дашбордов. Наaggregatedпропадает разбивкаby (name)— и вы видите «что-то банится», но не что именно.- Метрики CrowdSec — счётчики (
_total), а не gauge. Их надо оборачивать вrate()/increase(), иначе на графике будет вечно растущая лесенка. Исключение —cs_active_decisions,cs_buckets,cs_info: это gauge, их берём как есть. - Эндпоинты
/metricsнаружу не публикуем. 6060 (crowdsec) и 9090 (prometheus) живут только внутри docker-сети. - Не пугайтесь нулевого
cs_bucket_overflowed. Если сканеров сейчас нет — он и не растёт. Смотрите наcs_syslogsource_hits_total: если поток логов идёт, а вёдра не переполняются — значит просто тихо, а не «сломалось». - Баунсер не инструментирован. Сверять «решения CrowdSec ↔ реальный address-list роутера» приходится глазами (
/ip firewall address-list print count-only where list~"crowdsec"на роутере). Кто хочет закрыть и это — см. ниже.
Куда расти
- Метрики самого MikroTik. Поднять рядом
mikrotik-exporter(RouterOS API → Prometheus) или снимать по SNMP — тогда в Grafana появится реальный размерaddress-list, счётчики дропов по правилам файрвола, нагрузка/трафик на WAN. Это закроет последнюю «слепую зону»: не «сколько CrowdSec хочет забанить», а «сколько реально лежит на роутере и сколько пакетов оно срезало». - Alerting. Навесить правила в Grafana/Prometheus Alertmanager: «поток syslog упал в 0 на 10 минут» (детект умер), «память CrowdSec > X», «всплеск overflowed». У меня уведомления уходят в ntfy — но это уже отдельная история.
- Инструментировать баунсер. Добавить в питоновский скрипт
prometheus_clientи отдавать свои счётчики (добавлено/убрано за цикл, размер списка на роутере, ошибки коннекта к API).
Как обычно, дополню: набор метрик, панелей и алертов здесь — отправная точка под мой кейс. Дашборды, пороги, состав экспортёров спокойно крутятся под ваши задачи и железо — это каркас, масштабируйте под себя.
Вопросы и замечания — как всегда, велкам.