Обновить docs/services/lxc208-manage/alert/README.md

This commit is contained in:
zailon 2026-07-05 12:06:10 +05:00
parent 6381c44ef7
commit 6e69b9afc9

View File

@ -1,15 +1,63 @@
# Matrix Alert Relay — Документация # Matrix Alert Relay — Документация
## 1. Архитектура и структура проекта ## 1. Архитектура и структура проекта
Проект представляет собой HTTP-сервер (вебхук-релей), который принимает POST-запросы от систем мониторинга, парсит их, форматирует и отправляет в нужные комнаты Matrix. Проект представляет собой HTTP-сервер (вебхук-релей), который принимает POST-запросы от систем мониторинга, парсит их, форматирует и отправляет в нужные комнаты Matrix.
**Основные файлы:** ### 1.1. Общая схема
```mermaid
flowchart LR
subgraph VPS["VPS (Charon-vpn)"]
CS["CrowdSec<br/>━━━━━━━━━━━━<br/>• ssh-bf<br/>• ssh-slow-bf<br/>• ssh-bf_user-enum<br/>• 3xui-tls-errors<br/>• http-bad-user-agent<br/>• http-cve"]
SM["security-monitor.sh<br/>━━━━━━━━━━━━<br/>• ssh-successful-login<br/>• service-disabled<br/>• sudo-escalation"]
end
subgraph PVE["Proxmox VE / PBS"]
PBS["PBS Backup Hooks<br/>━━━━━━━━━━━━<br/>• Backup Success<br/>• Backup Failed<br/>• Backup older than 24h"]
end
subgraph Manage["Manage (Home Server)"]
GRAF["Grafana / Prometheus<br/>━━━━━━━━━━━━<br/>• HDD Temp<br/>• HDD Bad Blocks<br/>• Disk SMART Failed<br/>• Uncorrected Read Errors<br/>• ZFS Pool Offline<br/>• UPS Battery Low"]
end
subgraph Gateway["Gateway (Olimp)"]
NPM["NPM Reverse Proxy<br/>security.zailon.ru<br/>━━━━━━━━━━━━<br/>SSL Termination<br/>HTTP → 192.168.1.208:8086"]
end
subgraph Relay["matrix-relay :8086"]
MR["matrix-relay.py<br/>━━━━━━━━━━━━<br/>• crowdsec_alerts → crowdsec.py<br/>• alerts → grafana.py<br/>• backup source → pbs.py"]
end
subgraph Matrix["Matrix Homeserver"]
SEC["🔒 security"]
DSK["💾 disk"]
BKP["📦 backup"]
UPS["🔋 ups"]
DEF["💬 default"]
end
CS -->|POST JSON<br/>crowdsec_alerts| NPM
SM -->|POST JSON<br/>crowdsec_alerts| NPM
PBS -->|POST JSON<br/>backup format| MR
GRAF -->|POST JSON<br/>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-сервер, принимающий запросы и маршрутизирующий их. - `matrix-relay.py` — точка входа. HTTP-сервер, принимающий запросы и маршрутизирующий их.
- `config.py` — глобальные настройки (адрес Homeserver, токен, порт). - `config.py` — глобальные настройки (адрес Homeserver, токен, порт).
- `rooms.py` — маппинг логических имен каналов (`disk`, `backup`, `docker`) на ID комнат Matrix. - `rooms.py` — маппинг логических имен каналов (`disk`, `backup`, `docker`, `security`) на ID комнат Matrix.
- `alerts.py` — словарь `ALERTS_CONFIG` с описанием всех известных типов алертов. - `alerts.py` — словарь `ALERTS_CONFIG` с описанием всех известных типов алертов.
- `grafana.py` — обработчик стандартных вебхуков от Grafana/Prometheus. - `grafana.py` — обработчик стандартных вебхуков от Grafana/Prometheus.
- `pbs.py` — обработчик специфичных вебхуков от Proxmox VE / Proxmox Backup Server. - `pbs.py` — обработчик специфичных вебхуков от Proxmox VE / Proxmox Backup Server.
- `crowdsec.py` — обработчик вебхуков от CrowdSec.
- `formatters.py` — функции красивого форматирования текста для специфичных алертов. - `formatters.py` — функции красивого форматирования текста для специфичных алертов.
- `matrix.py` — низкоуровневая отправка HTML-сообщений в Matrix API. - `matrix.py` — низкоуровневая отправка HTML-сообщений в Matrix API.
- `utils.py` — вспомогательные утилиты (экранирование HTML, форматирование байтов и т.д.). - `utils.py` — вспомогательные утилиты (экранирование HTML, форматирование байтов и т.д.).
@ -17,21 +65,27 @@
--- ---
## 2. Сценарий А: Добавление нового алерта (Grafana / Prometheus) ## 2. Сценарий А: Добавление нового алерта (Grafana / Prometheus)
Если новый алерт приходит из Grafana или Prometheus, писать новый код парсинга не нужно. Достаточно обновить конфигурацию. Если новый алерт приходит из Grafana или Prometheus, писать новый код парсинга не нужно. Достаточно обновить конфигурацию.
### Шаг 2.1. Добавление комнаты (если нужна новая) ### Шаг 2.1. Добавление комнаты (если нужна новая)
Открой `rooms.py` и добавь новое имя канала и ID комнаты: Открой `rooms.py` и добавь новое имя канала и ID комнаты:
```python ```python
CHANNELS = { CHANNELS = {
"default": "!WlzSAzmLQEATwDCaBU:matrix.zailon.ru", "default": "!WlzSAzmLQEATwDCaBU:matrix.zailon.ru",
"disk": "!wOlscPjqPWVbSHpYgc:matrix.zailon.ru", "disk": "!wOlscPjqPWVbSHpYgc:matrix.zailon.ru",
"backup": "!ViUbvoFxTTbRLMtZEO:matrix.zailon.ru", "backup": "!ViUbvoFxTTbRLMtZEO:matrix.zailon.ru",
"security": "!SecurityRoomId:matrix.zailon.ru",
"ups": "!NewRoomId:matrix.zailon.ru", "ups": "!NewRoomId:matrix.zailon.ru",
} }
``` ```
### Шаг 2.2. Создание форматтера (опционально) ### Шаг 2.2. Создание форматтера (опционально)
Если алерт требует особого вывода (например, вывод заряда батареи в процентах), открой `formatters.py`: Если алерт требует особого вывода (например, вывод заряда батареи в процентах), открой `formatters.py`:
```python ```python
def ups_battery(device, value=None): def ups_battery(device, value=None):
if value is not None: if value is not None:
@ -45,7 +99,9 @@ FORMATTERS = {
``` ```
### Шаг 2.3. Регистрация алерта ### Шаг 2.3. Регистрация алерта
Открой `alerts.py` и добавь новый ключ в `ALERTS_CONFIG`. Имя ключа должно точно совпадать с `alertname` из Grafana. Открой `alerts.py` и добавь новый ключ в `ALERTS_CONFIG`. Имя ключа должно точно совпадать с `alertname` из Grafana.
```python ```python
ALERTS_CONFIG = { ALERTS_CONFIG = {
"UPS Battery Low": { "UPS Battery Low": {
@ -59,15 +115,19 @@ ALERTS_CONFIG = {
}, },
} }
``` ```
Релей автоматически подхватит новый алерт, отформатирует его и отправит в нужную комнату. Релей автоматически подхватит новый алерт, отформатирует его и отправит в нужную комнату.
--- ---
## 3. Сценарий Б: Добавление нового ИСТОЧНИКА алертов (кастомный вебхук) ## 3. Сценарий Б: Добавление нового ИСТОЧНИКА алертов (кастомный вебхук)
Если новая система (например, Docker Events, Zabbix, или скрипт мониторинга UPS) шлет свой собственный JSON, не похожий на формат Grafana, нужно написать новый модуль. Если новая система (например, Docker Events, Zabbix, или скрипт мониторинга UPS) шлет свой собственный JSON, не похожий на формат Grafana, нужно написать новый модуль.
### Шаг 3.1. Создание модуля-обработчика ### Шаг 3.1. Создание модуля-обработчика
Создай новый файл, например `ups.py` (по аналогии с `pbs.py`). Создай новый файл, например `ups.py` (по аналогии с `pbs.py`).
```python ```python
import logging import logging
from alerts.matrix import send_matrix from alerts.matrix import send_matrix
@ -93,7 +153,9 @@ def process_ups_alert(obj: dict) -> None:
``` ```
### Шаг 3.2. Подключение обработчика в роутер ### Шаг 3.2. Подключение обработчика в роутер
Открой `matrix-relay.py`. В методе `process_webhook` добавь условие для определения нового источника и вызови свою функцию. Открой `matrix-relay.py`. В методе `process_webhook` добавь условие для определения нового источника и вызови свою функцию.
```python ```python
from alerts.ups import process_ups_alert from alerts.ups import process_ups_alert
@ -123,31 +185,709 @@ class WebhookHandler(BaseHTTPRequestHandler):
"</pre>" "</pre>"
) )
``` ```
Теперь при отправке POST-запроса на сервер с JSON `{"ups_name": "APC", "battery": 10, "status": "critical"}` алерт улетит в Matrix. Теперь при отправке POST-запроса на сервер с JSON `{"ups_name": "APC", "battery": 10, "status": "critical"}` алерт улетит в Matrix.
--- ---
## 4. Шпаргалка по настройке вебхуков ## 4. Сценарий В: Интеграция CrowdSec (VPS мониторинг безопасности)
- **URL для всех источников:** `http://<IP-сервера-manage>:8086/` (или `https://`, если стоит реверс-прокси).
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 <b>Host:</b> {host}")
lines.append(f"\U0001F4CD <b>\u0426\u0435\u043b\u044c:</b> {scope}:{value}")
lines.append(f"\u26A1 <b>\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435:</b> \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 <b>Host:</b> {host}")
if scope and value:
lines.append(f"\U0001F4CD <b>\u0426\u0435\u043b\u044c:</b> {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 <b>Host:</b> {host}")
lines.append(f"\U0001F6E1 <b>\u0421\u0438\u0441\u0442\u0435\u043c\u0430:</b> {value}")
lines.append(f"\u26A1 <b>\u0421\u0442\u0430\u0442\u0443\u0441:</b> \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 <b>Host:</b> {host}")
lines.append(f"\U0001F464 <b>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c:</b> {value}")
lines.append(f"\U0001F4DD <b>\u041a\u043e\u043c\u0430\u043d\u0434\u0430:</b> {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"<b>{title}</b>", ""]
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(
"<b>Webhook received</b><br><br><pre>" +
json.dumps(obj, ensure_ascii=False, indent=2) +
"</pre>"
)
```
**Теория:**
- Проверка `"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://<IP-сервера-manage>:8086/` (напрямую)
- **Метод:** POST - **Метод:** POST
- **Content-Type:** `application/json` - **Content-Type:** `application/json`
### Пример вызова из bash-скрипта (для Docker, UPS, Cron): ### Пример вызова из bash-скрипта (для Docker, UPS, Cron):
```bash ```bash
curl -X POST http://127.0.0.1:8086/ \ curl -X POST https://security.zailon.ru/ \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}' -d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}'
``` ```
--- ---
## 5. Логирование и отладка ## 6. Логирование и отладка
Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи: Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи:
```bash ```bash
journalctl -u matrix-relay -f journalctl -u matrix-relay -f
``` ```
Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки: Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки:
- `Processing Grafana alerts` — релей распознал формат Grafana. - `Processing CrowdSec alert` — релей распознал формат CrowdSec
- `Processing backup notification` — релей распознал бэкап PVE/PBS. - `Processing Grafana alerts` — релей распознал формат Grafana
- `Processing backup notification` — релей распознал бэкап PVE/PBS
- `Unknown webhook type` — релей не нашел знакомых ключей и отправил "сырой" JSON в комнату `default`. Используй это для отладки новых источников! - `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"`