# 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 (Olimp)"] 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`, `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, форматирование байтов и т.д.). --- ## 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: return f"🔋 {device:<8} {value}%" return f"🔋 {device:<8}" FORMATTERS = { "temperature": temperature, "ups_battery": ups_battery, } ``` ### Шаг 2.3. Регистрация алерта Открой `alerts.py` и добавь новый ключ в `ALERTS_CONFIG`. Имя ключа должно точно совпадать с `alertname` из Grafana. ```python ALERTS_CONFIG = { "UPS Battery Low": { "room": "ups", "title": "🔋 Низкий заряд ИБП", "summary": "⚡ Заряд батареи ИБП упал ниже критического.", "info": "🔌 Проверьте питание и состояние ИБП.", "resolved": "✅ Питание ИБП восстановлено.", "formatter": "ups_battery", "skip_zero_firing": False, }, } ``` Релей автоматически подхватит новый алерт, отформатирует его и отправит в нужную комнату. --- ## 3. Сценарий Б: Добавление нового ИСТОЧНИКА алертов (кастомный вебхук) Если новая система (например, Docker Events, Zabbix, или скрипт мониторинга UPS) шлет свой собственный JSON, не похожий на формат Grafana, нужно написать новый модуль. ### Шаг 3.1. Создание модуля-обработчика Создай новый файл, например `ups.py` (по аналогии с `pbs.py`). ```python import logging from alerts.matrix import send_matrix logger = logging.getLogger(__name__) def process_ups_alert(obj: dict) -> None: ups_name = obj.get("ups_name", "Unknown UPS") battery_level = obj.get("battery", 0) status = obj.get("status", "unknown") if status == "critical": text = f"🚨 Критический разряд ИБП\n\n" text += f"🔋 {ups_name}\n" text += f"📉 Осталось заряда: {battery_level}%\n" text += "⚡ Срочно проверьте питание!" room = "ups" else: text = f"ℹ️ Статус ИБП\n\n{ups_name}: {battery_level}%" room = "default" send_matrix(text, room) ``` ### Шаг 3.2. Подключение обработчика в роутер Открой `matrix-relay.py`. В методе `process_webhook` добавь условие для определения нового источника и вызови свою функцию. ```python from alerts.ups import process_ups_alert class WebhookHandler(BaseHTTPRequestHandler): def process_webhook(self, obj: Dict) -> None: if "ups_name" in obj: logger.info("Processing UPS notification") process_ups_alert(obj) return source = detect_backup_source(obj) if source: logger.info("Processing backup notification (source=%s)", source.value) process_backup(obj, send_matrix, source) 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) +
            "
" ) ``` Теперь при отправке POST-запроса на сервер с JSON `{"ups_name": "APC", "battery": 10, "status": "critical"}` алерт улетит в Matrix. --- ## 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 https://security.zailon.ru/ \ -H "Content-Type: application/json" \ -d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}' ``` --- ## 6. Логирование и отладка Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи: ```bash journalctl -u matrix-relay -f ``` Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки: - `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"`