diff --git a/docs/services/lxc208-manage/alert/README.md b/docs/services/lxc208-manage/alert/README.md index 17ecd9e..fd1b48a 100644 --- a/docs/services/lxc208-manage/alert/README.md +++ b/docs/services/lxc208-manage/alert/README.md @@ -1,15 +1,63 @@ # Matrix Alert Relay — Документация ## 1. Архитектура и структура проекта + Проект представляет собой HTTP-сервер (вебхук-релей), который принимает POST-запросы от систем мониторинга, парсит их, форматирует и отправляет в нужные комнаты Matrix. -**Основные файлы:** +### 1.1. Общая схема + +```mermaid +flowchart LR + subgraph VPS["VPS (Charon-vpn)"] + CS["CrowdSec
━━━━━━━━━━━━
• ssh-bf
• ssh-slow-bf
• ssh-bf_user-enum
• 3xui-tls-errors
• http-bad-user-agent
• http-cve"] + SM["security-monitor.sh
━━━━━━━━━━━━
• ssh-successful-login
• service-disabled
• sudo-escalation"] + end + + subgraph PVE["Proxmox VE / PBS"] + PBS["PBS Backup Hooks
━━━━━━━━━━━━
• Backup Success
• Backup Failed
• Backup older than 24h"] + end + + subgraph Manage["Manage (Home Server)"] + GRAF["Grafana / Prometheus
━━━━━━━━━━━━
• HDD Temp
• HDD Bad Blocks
• Disk SMART Failed
• Uncorrected Read Errors
• ZFS Pool Offline
• UPS Battery Low"] + end + + subgraph Gateway["Gateway (Olimp)"] + NPM["NPM Reverse Proxy
security.zailon.ru
━━━━━━━━━━━━
SSL Termination
HTTP → 192.168.1.208:8086"] + end + + subgraph Relay["matrix-relay :8086"] + MR["matrix-relay.py
━━━━━━━━━━━━
• crowdsec_alerts → crowdsec.py
• alerts → grafana.py
• backup source → pbs.py"] + end + + subgraph Matrix["Matrix Homeserver"] + SEC["🔒 security"] + DSK["💾 disk"] + BKP["📦 backup"] + UPS["🔋 ups"] + DEF["💬 default"] + end + + CS -->|POST JSON
crowdsec_alerts| NPM + SM -->|POST JSON
crowdsec_alerts| NPM + PBS -->|POST JSON
backup format| MR + GRAF -->|POST JSON
alerts| MR + NPM -->|HTTP forward| MR + MR -->|"security"| SEC + MR -->|"disk"| DSK + MR -->|"backup"| BKP + MR -->|"ups"| UPS + MR -->|"default"| DEF +``` + +### 1.2. Основные файлы + - `matrix-relay.py` — точка входа. HTTP-сервер, принимающий запросы и маршрутизирующий их. - `config.py` — глобальные настройки (адрес Homeserver, токен, порт). -- `rooms.py` — маппинг логических имен каналов (`disk`, `backup`, `docker`) на ID комнат Matrix. +- `rooms.py` — маппинг логических имен каналов (`disk`, `backup`, `docker`, `security`) на ID комнат Matrix. - `alerts.py` — словарь `ALERTS_CONFIG` с описанием всех известных типов алертов. - `grafana.py` — обработчик стандартных вебхуков от Grafana/Prometheus. - `pbs.py` — обработчик специфичных вебхуков от Proxmox VE / Proxmox Backup Server. +- `crowdsec.py` — обработчик вебхуков от CrowdSec. - `formatters.py` — функции красивого форматирования текста для специфичных алертов. - `matrix.py` — низкоуровневая отправка HTML-сообщений в Matrix API. - `utils.py` — вспомогательные утилиты (экранирование HTML, форматирование байтов и т.д.). @@ -17,21 +65,27 @@ --- ## 2. Сценарий А: Добавление нового алерта (Grafana / Prometheus) + Если новый алерт приходит из Grafana или Prometheus, писать новый код парсинга не нужно. Достаточно обновить конфигурацию. ### Шаг 2.1. Добавление комнаты (если нужна новая) + Открой `rooms.py` и добавь новое имя канала и ID комнаты: + ```python CHANNELS = { "default": "!WlzSAzmLQEATwDCaBU:matrix.zailon.ru", "disk": "!wOlscPjqPWVbSHpYgc:matrix.zailon.ru", "backup": "!ViUbvoFxTTbRLMtZEO:matrix.zailon.ru", + "security": "!SecurityRoomId:matrix.zailon.ru", "ups": "!NewRoomId:matrix.zailon.ru", } ``` ### Шаг 2.2. Создание форматтера (опционально) + Если алерт требует особого вывода (например, вывод заряда батареи в процентах), открой `formatters.py`: + ```python def ups_battery(device, value=None): if value is not None: @@ -45,7 +99,9 @@ FORMATTERS = { ``` ### Шаг 2.3. Регистрация алерта + Открой `alerts.py` и добавь новый ключ в `ALERTS_CONFIG`. Имя ключа должно точно совпадать с `alertname` из Grafana. + ```python ALERTS_CONFIG = { "UPS Battery Low": { @@ -59,15 +115,19 @@ ALERTS_CONFIG = { }, } ``` + Релей автоматически подхватит новый алерт, отформатирует его и отправит в нужную комнату. --- ## 3. Сценарий Б: Добавление нового ИСТОЧНИКА алертов (кастомный вебхук) + Если новая система (например, Docker Events, Zabbix, или скрипт мониторинга UPS) шлет свой собственный JSON, не похожий на формат Grafana, нужно написать новый модуль. ### Шаг 3.1. Создание модуля-обработчика + Создай новый файл, например `ups.py` (по аналогии с `pbs.py`). + ```python import logging from alerts.matrix import send_matrix @@ -93,7 +153,9 @@ def process_ups_alert(obj: dict) -> None: ``` ### Шаг 3.2. Подключение обработчика в роутер + Открой `matrix-relay.py`. В методе `process_webhook` добавь условие для определения нового источника и вызови свою функцию. + ```python from alerts.ups import process_ups_alert @@ -123,31 +185,709 @@ class WebhookHandler(BaseHTTPRequestHandler): "" ) ``` + Теперь при отправке POST-запроса на сервер с JSON `{"ups_name": "APC", "battery": 10, "status": "critical"}` алерт улетит в Matrix. --- -## 4. Шпаргалка по настройке вебхуков -- **URL для всех источников:** `http://:8086/` (или `https://`, если стоит реверс-прокси). +## 4. Сценарий В: Интеграция CrowdSec (VPS мониторинг безопасности) + +CrowdSec — система обнаружения и блокировки атак. На VPS установлен CrowdSec, который автоматически банит IP за brute-force, CVE-эксплойты и другие атаки. Мы настроили отправку уведомлений в Matrix при каждом бане. + +### 4.1. Архитектура интеграции + +1. **CrowdSec** на VPS детектирует атаку и выносит решение (ban/alert) +2. **Notification plugin** отправляет POST-запрос на `https://security.zailon.ru/` +3. **NPM** на Gateway форвардит запрос на `192.168.1.208:8086` (manage-сервер) +4. **matrix-relay** принимает запрос, парсит и отправляет в комнату `security` в Matrix + +### 4.2. Настройка CrowdSec на VPS + +#### Шаг 4.2.1. Создание notification config + +Создай файл `/etc/crowdsec/notifications/matrix-relay.yaml`: + +```yaml +type: http +name: matrix_relay +method: POST +format: | + { + "crowdsec_alerts": [ + {{- range $index, $alert := . }} + { + "scenario": "{{ $alert.Scenario }}", + "value": "{{ $alert.Source.Value }}", + "scope": "{{ $alert.Source.Scope }}", + "action": "{{ if $alert.Remediation }}ban{{ else }}alert{{ end }}", + "reason": "{{ $alert.Message }}", + "host": "Charon-vpn" + }{{ if ne $index (sub (len $) 1) }},{{ end }} + {{- end }} + ] + } +group_wait: 10s +group_by: + - scenario + - source +max_retry: 3 +timeout: 10s +headers: + Content-Type: application/json +url: https://security.zailon.ru/ +``` + +**Теория:** +- `type: http` — используем HTTP плагин для отправки +- `method: POST` — обязательно POST, иначе relay вернёт 501 +- `format` — Go-шаблон, формирующий JSON для relay. Переменная `host` — имя сервера для отображения в алерте +- `group_wait: 10s` — ждём 10 секунд, чтобы сгруппировать несколько событий в один алерт +- `group_by` — группируем по сценарию и источнику +- `url` — endpoint relay (через NPM reverse proxy) + +#### Шаг 4.2.2. Настройка profiles + +Открой `/etc/crowdsec/profiles.yaml` и добавь `notifications: - matrix_relay` в профили, которые выносят решения: + +```yaml +name: default_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" +decisions: + - type: ban + duration: 4h +notifications: + - matrix_relay +on_success: break +--- +name: default_range_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" +decisions: + - type: ban + duration: 4h +notifications: + - matrix_relay +on_success: break +``` + +**Теория:** +- `notifications: - matrix_relay` — при срабатывании профиля CrowdSec вызовет notification plugin +- `on_success: break` — после применения профиля прекращаем обработку (важно, иначе могут сработать другие профили) + +#### Шаг 4.2.3. Создание whitelist + +Создай файл `/etc/crowdsec/whitelist.txt` для IP, которые не должны триггерить алерты: + +``` +188.73.191.202 +2.27.50.20 +127.0.0.1 +``` + +**Теория:** +- Это whitelist для `security-monitor.sh` (см. ниже), не для CrowdSec +- CrowdSec использует свои whitelist в `/etc/crowdsec/parsers/` + +### 4.3. Настройка security-monitor.sh на VPS + +CrowdSec отлично справляется с паттернами в логах (брутфорс, веб-атаки), но для системных событий (остановка сервисов, sudo) используем bash-скрипт, который мониторит логи и отправляет вебхуки напрямую в relay. + +#### Шаг 4.3.1. Создание скрипта + +Создай файл `/usr/local/bin/security-monitor.sh`: + +```bash +#!/bin/bash + +RELAY_URL="https://security.zailon.ru/" +HOST_NAME="Charon-vpn" +FAILED_IPS_FILE="/tmp/failed_ssh_ips.txt" +WHITELIST_FILE="/etc/crowdsec/whitelist.txt" +STATE_FILE="/tmp/ssh-monitor-state.txt" + +send_alert() { + local scenario="$1" + local value="$2" + local scope="$3" + local reason="$4" + + curl -s -X POST "$RELAY_URL" \ + -H "Content-Type: application/json" \ + -d "{ + \"crowdsec_alerts\": [{ + \"scenario\": \"$scenario\", + \"value\": \"$value\", + \"scope\": \"$scope\", + \"action\": \"alert\", + \"reason\": \"$reason\", + \"host\": \"$HOST_NAME\" + }] + }" > /dev/null 2>&1 +} + +is_whitelisted() { + grep -qxF "$1" "$WHITELIST_FILE" 2>/dev/null +} + +check_recent_login() { + local ip="$1" + local current_time=$(date +%s) + + if [ -f "$STATE_FILE" ]; then + while IFS=' ' read -r stored_ip stored_time; do + if [ "$stored_ip" = "$ip" ]; then + local diff=$((current_time - stored_time)) + if [ $diff -lt 30 ]; then + return 0 + fi + fi + done < "$STATE_FILE" + fi + + echo "$ip $current_time" >> "$STATE_FILE" + tail -20 "$STATE_FILE" > /tmp/state_tmp.txt && mv /tmp/state_tmp.txt "$STATE_FILE" + return 1 +} + +journalctl -u ssh.service -f --since now --no-pager | while read line; do + if echo "$line" | grep -q "Accepted"; then + IP=$(echo "$line" | grep -oP 'from \K[\d.]+') + USER=$(echo "$line" | grep -oP 'for \K\S+') + + if check_recent_login "$IP"; then + continue + fi + + if is_whitelisted "$IP"; then + continue + fi + + if grep -q "$IP" "$FAILED_IPS_FILE" 2>/dev/null; then + REASON="SSH login by $USER (after failed attempts!)" + sed -i "/$IP/d" "$FAILED_IPS_FILE" + else + REASON="SSH login by $USER" + fi + + send_alert "ssh-successful-login" "$IP" "Ip" "$REASON" + elif echo "$line" | grep -q "Failed password"; then + IP=$(echo "$line" | grep -oP 'from \K[\d.]+') + echo "$IP" >> "$FAILED_IPS_FILE" + [ $(wc -l < "$FAILED_IPS_FILE") -gt 1000 ] && > "$FAILED_IPS_FILE" + fi +done & + +journalctl -u crowdsec.service -u ufw.service -u auditd.service -u docker-logs-converter.service -f --since now --no-pager | grep --line-buffered "Stopped" | while read line; do + SERVICE=$(echo "$line" | grep -oP 'Stopped \K\S+' | sed 's/\.service//') + [ -n "$SERVICE" ] && send_alert "service-disabled" "$SERVICE" "Service" "Protective service stopped" +done & + +tail -F /var/log/auth.log 2>/dev/null | grep --line-buffered "sudo:" | grep --line-buffered "COMMAND=" | while read line; do + USER=$(echo "$line" | grep -oP 'sudo:\s+\K\S+') + CMD=$(echo "$line" | grep -oP 'COMMAND=\K.*') + + [ "$USER" = "root" ] && continue + + send_alert "sudo-escalation" "$USER" "User" "$CMD" +done & + +wait +``` + +**Теория:** +- `send_alert()` — функция отправки вебхука в relay с форматом `crowdsec_alerts` +- `is_whitelisted()` — проверяет, есть ли IP в whitelist +- `check_recent_login()` — дедупликация: если с того же IP было подключение в последние 30 секунд, игнорируем +- Первый `journalctl` блок — мониторит SSH логины (успешные и неудачные) +- Второй `journalctl` блок — мониторит остановку защитных сервисов (crowdsec, ufw, auditd, docker-logs-converter) +- Третий `tail -F` блок — мониторит sudo команды от не-root пользователей +- `--since now` — читаем только новые события, не историю +- `--line-buffered` — для grep, чтобы обрабатывать строки сразу, не буферизируя + +#### Шаг 4.3.2. Создание systemd сервиса + +Создай файл `/etc/systemd/system/security-monitor.service`: + +```ini +[Unit] +Description=Security Events Monitor +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/security-monitor.sh +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +**Теория:** +- `Restart=always` — автоматический перезапуск при падении +- `RestartSec=10` — пауза 10 секунд перед перезапуском + +#### Шаг 4.3.3. Запуск сервиса + +```bash +chmod +x /usr/local/bin/security-monitor.sh +systemctl daemon-reload +systemctl enable --now security-monitor +systemctl status security-monitor --no-pager +``` + +### 4.4. Настройка relay на manage-сервере + +#### Шаг 4.4.1. Добавление сценариев в alerts.py + +Открой `/usr/local/bin/alerts/alerts.py` и добавь конфигурацию CrowdSec алертов: + +```python +ALERTS_CONFIG = { + # ... существующие алерты ... + + "3xui-tls-errors": { + "room": "security", + "title": "\U0001F525 CrowdSec: TLS Errors", + "summary": "\U0001F525 \u041c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 TLS handshake", + "info": "\U0001F6E0 \u041f\u0440\u043e\u0432\u0435\u0440\u044c \u043b\u043e\u0433\u0438 3x-ui \u043d\u0430 \u043f\u0440\u0435\u0434\u043c\u0435\u0442 \u0430\u0442\u0430\u043a", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, + }, + "3xui-successful-login": { + "room": "security", + "title": "\u26A0\uFE0F CrowdSec: \u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0432\u0445\u043e\u0434 3x-ui", + "summary": "\U0001F464 \u0423\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u043f\u0430\u043d\u0435\u043b\u0438 3x-ui", + "formatter": "crowdsec_alert", + "skip_zero_firing": False, + }, + "crowdsecurity/ssh-bf": { + "room": "security", + "title": "\U0001F525 CrowdSec: SSH Brute-Force", + "summary": "\U0001F511 \u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 SSH brute-force \u0430\u0442\u0430\u043a\u0430", + "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, + }, + "crowdsecurity/ssh-slow-bf": { + "room": "security", + "title": "\U0001F525 CrowdSec: SSH Slow Brute-Force", + "summary": "\U0001F511 \u041c\u0435\u0434\u043b\u0435\u043d\u043d\u0430\u044f SSH brute-force \u0430\u0442\u0430\u043a\u0430", + "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, + }, + "crowdsecurity/ssh-bf_user-enum": { + "room": "security", + "title": "\U0001F525 CrowdSec: SSH User Enumeration", + "summary": "\U0001F464 \u041f\u0435\u0440\u0435\u0431\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 SSH", + "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, + }, + "crowdsecurity/http-bad-user-agent": { + "room": "security", + "title": "\U0001F525 CrowdSec: Bad User-Agent", + "summary": "\U0001F577\uFE0F \u041f\u043e\u0434\u043e\u0437\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 User-Agent", + "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, + }, + "ssh-successful-login": { + "room": "security", + "title": "\u2705 CrowdSec: SSH \u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0432\u0445\u043e\u0434", + "summary": "\U0001F511 \u0423\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e SSH", + "formatter": "crowdsec_alert", + "skip_zero_firing": False, + }, + "service-disabled": { + "room": "security", + "title": "\u26D4 CrowdSec: \u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0438\u0442\u044b", + "summary": "\U0001F6A8 \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u0437\u0430\u0449\u0438\u0442\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430", + "formatter": "crowdsec_service_disabled", + "skip_zero_firing": False, + }, + "sudo-escalation": { + "room": "security", + "title": "\u26A0\uFE0F CrowdSec: \u041f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0439", + "summary": "\U0001F511 \u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 root", + "formatter": "crowdsec_sudo", + "skip_zero_firing": False, + }, +} +``` + +**Теория:** +- Ключи словаря должны точно совпадать со значением `scenario` в JSON от CrowdSec +- `room: "security"` — комната Matrix для отправки +- `title` — заголовок алерта (используй Unicode escape для эмодзи) +- `formatter` — имя функции из `formatters.py` для форматирования тела сообщения + +#### Шаг 4.4.2. Добавление форматтеров в formatters.py + +Открой `/usr/local/bin/alerts/formatters.py` и добавь функции форматирования: + +```python +def crowdsec_ban(alert_info): + lines = [] + host = alert_info.get("host", "unknown") + value = alert_info.get("value", "") + scope = alert_info.get("scope", "") + lines.append(f"\U0001F5A5 Host: {host}") + lines.append(f"\U0001F4CD \u0426\u0435\u043b\u044c: {scope}:{value}") + lines.append(f"\u26A1 \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: \u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d") + return "\n".join(lines) + +def crowdsec_alert(alert_info): + lines = [] + host = alert_info.get("host", "unknown") + value = alert_info.get("value", "") + scope = alert_info.get("scope", "") + lines.append(f"\U0001F5A5 Host: {host}") + if scope and value: + lines.append(f"\U0001F4CD \u0426\u0435\u043b\u044c: {scope}:{value}") + return "\n".join(lines) + +def crowdsec_service_disabled(alert_info): + lines = [] + host = alert_info.get("host", "unknown") + value = alert_info.get("value", "") + lines.append(f"\U0001F5A5 Host: {host}") + lines.append(f"\U0001F6E1 \u0421\u0438\u0441\u0442\u0435\u043c\u0430: {value}") + lines.append(f"\u26A1 \u0421\u0442\u0430\u0442\u0443\u0441: \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430") + return "\n".join(lines) + +def crowdsec_sudo(alert_info): + lines = [] + host = alert_info.get("host", "unknown") + value = alert_info.get("value", "") + reason = alert_info.get("reason", "") + lines.append(f"\U0001F5A5 Host: {host}") + lines.append(f"\U0001F464 \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c: {value}") + lines.append(f"\U0001F4DD \u041a\u043e\u043c\u0430\u043d\u0434\u0430: {reason}") + return "\n".join(lines) + +FORMATTERS = { + "temperature": temperature, + "bad_blocks": bad_blocks, + "smart": smart, + "read_errors": read_errors, + "zfs_pool": zfs_pool, + "backup_outdated": backup_outdated, + "backup_success": backup_success, + "backup_failed": backup_failed, + "crowdsec_ban": crowdsec_ban, + "crowdsec_alert": crowdsec_alert, + "crowdsec_service_disabled": crowdsec_service_disabled, + "crowdsec_sudo": crowdsec_sudo, +} +``` + +**Теория:** +- Каждая функция принимает `alert_info` — словарь с данными из JSON +- Возвращает строку с HTML-форматированием для Matrix +- Используй Unicode escape (`\U0001F5A5`) для эмодзи, чтобы избежать проблем с кодировкой + +#### Шаг 4.4.3. Создание обработчика crowdsec.py + +Создай файл `/usr/local/bin/alerts/crowdsec.py`: + +```python +import logging +from typing import Dict + +from alerts.alerts import ALERTS_CONFIG +from alerts.utils import escape_html +from alerts.formatters import get_formatter + +logger = logging.getLogger(__name__) + +SECURITY_CHANNEL = "security" + +def process_crowdsec_alert(obj: Dict) -> None: + alerts = obj.get("crowdsec_alerts", []) + if not alerts: + logger.warning("No crowdsec_alerts in payload") + return + + from alerts.matrix import send_matrix + + for alert in alerts: + scenario = alert.get("scenario", "Unknown") + + config = ALERTS_CONFIG.get(scenario) + if not config: + logger.warning("Unknown CrowdSec scenario: %s", scenario) + continue + + room = config.get("room", SECURITY_CHANNEL) + title = config.get("title", f"CrowdSec Alert: {scenario}") + + formatter_name = config.get("formatter") + formatter = get_formatter(formatter_name) if formatter_name else None + + lines = [f"{title}", ""] + + if formatter: + lines.append(formatter(alert)) + else: + lines.append(f"\U0001F5A5 Host: {escape_html(alert.get('host', 'unknown'))}") + lines.append(f"\U0001F4CD Target: {escape_html(alert.get('scope', ''))}:{escape_html(alert.get('value', ''))}") + + if "summary" in config: + lines.append("") + lines.append(config["summary"]) + if "info" in config: + lines.append(config["info"]) + + send_matrix("\n".join(lines), room) +``` + +**Теория:** +- Функция `process_crowdsec_alert()` принимает JSON с ключом `crowdsec_alerts` +- Для каждого алерта ищет конфигурацию в `ALERTS_CONFIG` по имени сценария +- Применяет форматтер для тела сообщения +- Отправляет в комнату Matrix + +#### Шаг 4.4.4. Подключение обработчика в matrix-relay.py + +Открой `/usr/local/bin/alerts/matrix-relay.py` и добавь импорт и обработку: + +```python +from alerts.crowdsec import process_crowdsec_alert + +class WebhookHandler(BaseHTTPRequestHandler): + def process_webhook(self, obj: Dict) -> None: + source = detect_backup_source(obj) + if source: + logger.info("Processing backup notification (source=%s)", source.value) + process_backup(obj, send_matrix, source) + return + + if "crowdsec_alerts" in obj: + logger.info("Processing CrowdSec alert") + process_crowdsec_alert(obj) + return + + if "alerts" in obj: + alerts = obj["alerts"] + logger.info("Processing Grafana alerts: %d items", len(alerts)) + process_grafana({"alerts": alerts}, send_matrix) + return + + logger.warning("Unknown webhook type") + send_matrix( + "Webhook received

" +
+            json.dumps(obj, ensure_ascii=False, indent=2) +
+            "
" + ) +``` + +**Теория:** +- Проверка `"crowdsec_alerts" in obj` должна быть **перед** проверкой `"alerts" in obj`, иначе relay попытается обработать CrowdSec как Grafana +- Порядок проверок важен: сначала специфичные форматы, потом общие + +### 4.5. Настройка NPM Reverse Proxy на Gateway + +#### Шаг 4.5.1. Создание Proxy Host + +В NPM создай новый Proxy Host: +- **Domain Names:** `security.zailon.ru` +- **Scheme:** `http` +- **Forward Hostname/IP:** `192.168.1.208` +- **Forward Port:** `8086` +- **Cache Assets:** ☑ +- **Block Common Exploits:** ☑ +- **SSL:** Let's Encrypt (галочка Force SSL) + +**Теория:** +- NPM форвардит HTTPS запросы на `security.zailon.ru` на внутренний IP manage-сервера +- SSL terminates на NPM, дальше идёт HTTP +- Это позволяет VPS отправлять вебхуки через публичный домен без открытия портов на роутере + +### 4.6. Тестирование интеграции + +#### Шаг 4.6.1. Тест с VPS + +```bash +curl -X POST https://security.zailon.ru/ \ + -H "Content-Type: application/json" \ + -d '{ + "crowdsec_alerts": [{ + "scenario": "3xui-tls-errors", + "value": "1.2.3.4", + "scope": "Ip", + "action": "ban", + "reason": "test", + "host": "Charon-vpn" + }] + }' +``` + +Должно прийти сообщение в комнату `security` в Matrix. + +#### Шаг 4.6.2. Генерация тестового алерта CrowdSec + +```bash +> /var/log/3xui-access.log +for i in {1..10}; do + echo "$(date +'%Y/%m/%d %H:%M:%S') http: TLS handshake error from 198.51.100.99:12345: tls: client offered only unsupported versions" >> /var/log/3xui-access.log +done +``` + +Через 5-10 секунд должен прийти алерт о бане. + +#### Шаг 4.6.3. Проверка логов + +На VPS: +```bash +tail -20 /var/log/crowdsec.log | grep -i "notif\|http\|matrix" +``` + +На manage: +```bash +journalctl -u matrix-relay --no-pager -n 30 +``` + +--- + +## 5. Шпаргалка по настройке вебхуков + +- **URL для всех источников:** `https://security.zailon.ru/` (через NPM reverse proxy) или `http://:8086/` (напрямую) - **Метод:** POST - **Content-Type:** `application/json` ### Пример вызова из bash-скрипта (для Docker, UPS, Cron): + ```bash -curl -X POST http://127.0.0.1:8086/ \ +curl -X POST https://security.zailon.ru/ \ -H "Content-Type: application/json" \ -d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}' ``` --- -## 5. Логирование и отладка +## 6. Логирование и отладка + Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи: + ```bash journalctl -u matrix-relay -f ``` Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки: -- `Processing Grafana alerts` — релей распознал формат Grafana. -- `Processing backup notification` — релей распознал бэкап PVE/PBS. -- `Unknown webhook type` — релей не нашел знакомых ключей и отправил "сырой" JSON в комнату `default`. Используй это для отладки новых источников! \ No newline at end of file +- `Processing CrowdSec alert` — релей распознал формат CrowdSec +- `Processing Grafana alerts` — релей распознал формат Grafana +- `Processing backup notification` — релей распознал бэкап PVE/PBS +- `Unknown webhook type` — релей не нашел знакомых ключей и отправил "сырой" JSON в комнату `default`. Используй это для отладки новых источников! + +### Отладка CrowdSec на VPS: + +```bash +tail -50 /var/log/crowdsec.log | grep -i "notif\|http\|matrix" +``` + +Типичные ошибки: +- `HTTP server returned non 200 status code: 501` — relay не поддерживает GET, нужен `method: POST` в notification config +- `can't evaluate field Env` — в шаблоне используется `$.Env.HOSTNAME`, которого нет. Используй жёстко заданное имя хоста +- `notification 'matrix_relay' is defined multiple times` — дубликаты в `/etc/crowdsec/notifications/`, удали лишние + +### Отладка security-monitor.sh: + +```bash +systemctl status security-monitor +journalctl -u security-monitor -f +``` + +Для временной отладки добавь в скрипт: +```bash +DEBUG_LOG="/var/log/security-monitor-debug.log" +echo "$(date): ALERT $scenario $value" >> "$DEBUG_LOG" +``` + +--- + +## 7. Добавление новых сценариев CrowdSec + +### Шаг 7.1. Добавление нового сценария в alerts.py + +Открой `/usr/local/bin/alerts/alerts.py` и добавь новый ключ: + +```python +"crowdsecurity/new-scenario": { + "room": "security", + "title": "\U0001F6A8 CrowdSec: New Scenario", + "summary": "\U0001F525 \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f", + "info": "\U0001F6E0 \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438", + "formatter": "crowdsec_ban", + "skip_zero_firing": False, +}, +``` + +**Теория:** +- Имя ключа должно точно совпадать со значением `scenario` в JSON от CrowdSec +- Можно посмотреть доступные сценарии: `cscli scenarios list` на VPS + +### Шаг 7.2. Перезапуск relay + +```bash +systemctl restart matrix-relay +``` + +Новый сценарий подхватится автоматически, переписывать код не нужно. + +--- + +## 8. Управление whitelist + +### Добавление IP в whitelist: + +```bash +echo "1.2.3.4" >> /etc/crowdsec/whitelist.txt +``` + +### Просмотр whitelist: + +```bash +cat /etc/crowdsec/whitelist.txt +``` + +**Теория:** +- Whitelist используется только `security-monitor.sh` для фильтрации SSH-логинов +- CrowdSec использует свои whitelist в `/etc/crowdsec/parsers/` +- IP из whitelist не будут триггерить алерты об успешных SSH-входах + +--- + +## 9. Ключевые файлы + +### VPS (Charon-vpn): +- `/etc/crowdsec/notifications/matrix-relay.yaml` — notification config для CrowdSec +- `/etc/crowdsec/profiles.yaml` — профили с notification +- `/etc/crowdsec/whitelist.txt` — whitelist IP для security-monitor +- `/usr/local/bin/security-monitor.sh` — скрипт мониторинга системных событий +- `/etc/systemd/system/security-monitor.service` — systemd сервис + +### Manage (Home Server): +- `/usr/local/bin/alerts/matrix-relay.py` — HTTP сервер relay +- `/usr/local/bin/alerts/alerts.py` — конфигурация алертов +- `/usr/local/bin/alerts/formatters.py` — форматтеры сообщений +- `/usr/local/bin/alerts/crowdsec.py` — обработчик CrowdSec +- `/etc/systemd/system/matrix-relay.service` — systemd сервис + +### Gateway (Olimp): +- NPM Proxy Host: `security.zailon.ru` → `192.168.1.208:8086` + +--- + +## 10. Решённые проблемы + +1. **`$.Env.HOSTNAME` в шаблоне CrowdSec** → жёстко заданное имя хоста `"host": "Charon-vpn"` +2. **GET вместо POST в notification** → добавлен `method: POST` +3. **Дублирование алертов** → дедупликация по времени (30 сек) в `security-monitor.sh` +4. **CPU 100% от journalctl** → переключение на `tail -F` + правильный `journalctl --since now` +5. **Старые события из журнала** → `--since now` + state file +6. **Спам от своего IP** → whitelist `/etc/crowdsec/whitelist.txt` +7. **Кириллица в Python файлах** → Unicode escape последовательности (`\U0001F5A5`) +8. **Порядок проверок в relay** → `"crowdsec_alerts"` должен проверяться **перед** `"alerts"` \ No newline at end of file