Docs/docs/services/lxc208-manage/alert/README.md

893 lines
36 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Matrix Alert Relay — Документация
## 1. Архитектура и структура проекта
Проект представляет собой 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-сервер, принимающий запросы и маршрутизирующий их.
- `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"<b>🚨 Критический разряд ИБП</b>\n\n"
text += f"🔋 <b>{ups_name}</b>\n"
text += f"📉 Осталось заряда: <b>{battery_level}%</b>\n"
text += "⚡ Срочно проверьте питание!"
room = "ups"
else:
text = f"<b> Статус ИБП</b>\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(
"<b>Webhook received</b><br><br><pre>" +
json.dumps(obj, ensure_ascii=False, indent=2) +
"</pre>"
)
```
Теперь при отправке 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 <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
- **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"`