CrowdSec + MikroTik. Часть 4: наблюдаемость — Prometheus + Grafana

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: кого опрашивать

:light_bulb: Уже держите свой 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.

:warning: 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-проектах → по имени контейнера друг друга не видят. Два варианта:

  1. Подключить контейнеры к общей сети (без публикации портов наружу — чище всего):

    docker network connect <сеть_прометея> crowdsec
    

    После этого Prometheus достучится до crowdsec:6060.

  2. Опубликовать 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 (шаг панели), не надо хардкодить окно.


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

  1. level: full обязателен для осмысленных дашбордов. На aggregated пропадает разбивка by (name) — и вы видите «что-то банится», но не что именно.
  2. Метрики CrowdSec — счётчики (_total), а не gauge. Их надо оборачивать в rate()/increase(), иначе на графике будет вечно растущая лесенка. Исключение — cs_active_decisions, cs_buckets, cs_info: это gauge, их берём как есть.
  3. Эндпоинты /metrics наружу не публикуем. 6060 (crowdsec) и 9090 (prometheus) живут только внутри docker-сети.
  4. Не пугайтесь нулевого cs_bucket_overflowed. Если сканеров сейчас нет — он и не растёт. Смотрите на cs_syslogsource_hits_total: если поток логов идёт, а вёдра не переполняются — значит просто тихо, а не «сломалось».
  5. Баунсер не инструментирован. Сверять «решения 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).

Как обычно, дополню: набор метрик, панелей и алертов здесь — отправная точка под мой кейс. Дашборды, пороги, состав экспортёров спокойно крутятся под ваши задачи и железо — это каркас, масштабируйте под себя.

Вопросы и замечания — как всегда, велкам.