# 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
• critical-file-change
• package-installed
• new-systemd-service
• suspicious-outbound"]
MAM["mass-attack-monitor.sh
━━━━━━━━━━━━
• crowdsec/mass-attack
(>50 банов за 5 мин)"]
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
MAM -->|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 и security-monitor.
- `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-эксплойты и другие атаки. Дополнительно работает bash-скрипт `security-monitor.sh`, отслеживающий системные события (SSH логины, sudo, остановка сервисов, изменение критических файлов, установка пакетов, новые systemd-сервисы, подозрительные исходящие подключения). Отдельный скрипт `mass-attack-monitor.sh` отслеживает массовые атаки (>50 банов за 5 минут).
### 4.1. Архитектура интеграции
1. **CrowdSec** на VPS детектирует атаку и выносит решение (ban/alert)
2. **Notification plugin** отправляет POST-запрос на `https://security.zailon.ru/`
3. **security-monitor.sh** и **mass-attack-monitor.sh** отправляют вебхуки напрямую через curl
4. **NPM** на Gateway форвардит запрос на `192.168.1.208:8086` (manage-сервер)
5. **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, изменение критических файлов, установка пакетов, новые systemd-сервисы, подозрительные исходящие) используем 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"
PKG_STATE_FILE="/tmp/pkg-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
}
check_recent_package() {
local current_time=$(date +%s)
if [ -f "$PKG_STATE_FILE" ]; then
local last_time=$(cat "$PKG_STATE_FILE")
local diff=$((current_time - last_time))
if [ $diff -lt 10 ]; then
return 0
fi
fi
echo "$current_time" > "$PKG_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 &
inotifywait -m -e modify -e create -e delete -e attrib /etc/passwd /etc/shadow /root/.ssh/authorized_keys /etc/ssh/sshd_config /etc/crontab /var/spool/cron/crontabs/ 2>/dev/null | while read path event file; do
send_alert "critical-file-change" "${path}${file}" "File" "Critical file modified: $event"
done &
tail -F /var/log/apt/history.log 2>/dev/null | grep --line-buffered "^Commandline:" | while read line; do
if check_recent_package; then
continue
fi
CMD=$(echo "$line" | sed 's/^Commandline: //')
send_alert "package-installed" "$CMD" "Package" "APT command executed"
done &
inotifywait -m -e create /etc/systemd/system/ 2>/dev/null | while read path event file; do
if [[ "$file" == *.service ]]; then
send_alert "new-systemd-service" "$file" "Service" "New systemd service created"
fi
done &
while true; do
SUSPICIOUS=$(ss -tnp state established 2>/dev/null | awk 'NR>1 {print $5}' | grep -vE ':(80|443|53|22|8080|8086|3000|9090)$' | grep -vE '^(127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)' | head -1)
if [ -n "$SUSPICIOUS" ]; then
send_alert "suspicious-outbound" "$SUSPICIOUS" "Connection" "Suspicious outbound connection detected"
sleep 300
fi
sleep 60
done &
wait
```
**Теория:**
- `send_alert()` — функция отправки вебхука в relay с форматом `crowdsec_alerts`
- `is_whitelisted()` — проверяет, есть ли IP в whitelist
- `check_recent_login()` — дедупликация SSH: если с того же IP было подключение в последние 30 секунд, игнорируем
- `check_recent_package()` — дедупликация пакетов: если команда apt была в последние 10 секунд, игнорируем
- Первый `journalctl` блок — мониторит SSH логины (успешные и неудачные)
- Второй `journalctl` блок — мониторит остановку защитных сервисов (crowdsec, ufw, auditd, docker-logs-converter)
- Третий `tail -F` блок — мониторит sudo команды от не-root пользователей
- Четвёртый `inotifywait` блок — мониторит изменения критических файлов (`/etc/passwd`, `/etc/shadow`, `/root/.ssh/authorized_keys`, `/etc/ssh/sshd_config`, `/etc/crontab`, `/var/spool/cron/crontabs/`)
- Пятый `tail -F` блок — мониторит команды apt в `/var/log/apt/history.log`
- Шестой `inotifywait` блок — мониторит создание новых `.service` файлов в `/etc/systemd/system/`
- Седьмой `while true` блок — мониторит подозрительные исходящие подключения на нестандартные порты (исключены 80/443/53/22/8080/8086/3000/9090 и приватные сети)
- `--since now` — читаем только новые события, не историю
- `--line-buffered` — для grep, чтобы обрабатывать строки сразу, не буферизируя
- Для работы `inotifywait` нужен пакет `inotify-tools`: `apt install inotify-tools -y`
#### Шаг 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
```
#### Шаг 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. Настройка mass-attack-monitor.sh на VPS
Отдельный скрипт для детекции массовых атак — когда за 5 минут создаётся более 50 банов. Это может быть признаком координированной атаки с разных IP.
#### Шаг 4.4.1. Создание скрипта
Создай файл `/usr/local/bin/mass-attack-monitor.sh`:
```bash
#!/bin/bash
RELAY_URL="https://security.zailon.ru/"
HOST_NAME="Charon-vpn"
STATE_FILE="/tmp/mass-attack-state.txt"
THRESHOLD=50
INTERVAL=300
send_alert() {
local count="$1"
curl -s -X POST "$RELAY_URL" \
-H "Content-Type: application/json" \
-d "{
\"crowdsec_alerts\": [{
\"scenario\": \"crowdsec/mass-attack\",
\"value\": \"$count\",
\"scope\": \"System\",
\"action\": \"alert\",
\"reason\": \"Mass attack detected: $count bans in 5 minutes\",
\"host\": \"$HOST_NAME\"
}]
}" > /dev/null 2>&1
}
while true; do
CURRENT_TIME=$(date +%s)
COUNT=$(cscli decisions list --since "${INTERVAL}s" -o json 2>/dev/null | jq 'length')
if [ "$COUNT" -gt "$THRESHOLD" ]; then
LAST_ALERT_TIME=$(cat "$STATE_FILE" 2>/dev/null || echo "0")
TIME_DIFF=$((CURRENT_TIME - LAST_ALERT_TIME))
if [ "$TIME_DIFF" -gt "$INTERVAL" ]; then
send_alert "$COUNT"
echo "$CURRENT_TIME" > "$STATE_FILE"
fi
fi
sleep 60
done
```
**Теория:**
- `THRESHOLD=50` — порог срабатывания (50 банов за 5 минут)
- `INTERVAL=300` — окно наблюдения в секундах (5 минут)
- `cscli decisions list --since 300s` — получает список банов за последние 5 минут
- `STATE_FILE` — хранит время последней отправки, чтобы не спамить алертами
- Цикл `sleep 60` — проверка раз в минуту
- `jq` — утилита для работы с JSON, нужна установка: `apt install jq -y`
#### Шаг 4.4.2. Создание systemd сервиса
Создай файл `/etc/systemd/system/mass-attack-monitor.service`:
```ini
[Unit]
Description=Mass Attack Monitor
After=network.target crowdsec.service
[Service]
Type=simple
ExecStart=/usr/local/bin/mass-attack-monitor.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
#### Шаг 4.4.3. Запуск сервиса
```bash
chmod +x /usr/local/bin/mass-attack-monitor.sh
apt install jq -y
systemctl daemon-reload
systemctl enable --now mass-attack-monitor
systemctl status mass-attack-monitor --no-pager
```
### 4.5. Настройка relay на manage-сервере
#### Шаг 4.5.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,
},
"crowdsec/mass-attack": {
"room": "security",
"title": "\U0001F6A8 CrowdSec: \u041c\u0430\u0441\u0441\u043e\u0432\u0430\u044f \u0430\u0442\u0430\u043a\u0430",
"summary": "\U0001F525 \u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0431\u043e\u043b\u0435\u0435 50 \u0431\u0430\u043d\u043e\u0432 \u0437\u0430 5 \u043c\u0438\u043d\u0443\u0442",
"info": "\U0001F6E0 \u041f\u0440\u043e\u0432\u0435\u0440\u044c \u043b\u043e\u0433\u0438 \u043d\u0430 \u043f\u0440\u0435\u0434\u043c\u0435\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0430\u0442\u0430\u043a\u0438",
"formatter": "crowdsec_mass_attack",
"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,
},
"critical-file-change": {
"room": "security",
"title": "\U0001F5C2\uFE0F CrowdSec: \u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430",
"summary": "\U0001F4CB \u041a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0431\u044b\u043b \u0438\u0437\u043c\u0435\u043d\u0451\u043d",
"formatter": "crowdsec_file_change",
"skip_zero_firing": False,
},
"package-installed": {
"room": "security",
"title": "\U0001F4E5 CrowdSec: \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043f\u0430\u043a\u0435\u0442\u0430",
"summary": "\U0001F4E6 \u041d\u043e\u0432\u044b\u0439 \u043f\u0430\u043a\u0435\u0442 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 apt/dpkg",
"formatter": "crowdsec_package",
"skip_zero_firing": False,
},
"new-systemd-service": {
"room": "security",
"title": "\U0001F6E0 CrowdSec: \u041d\u043e\u0432\u044b\u0439 systemd \u0441\u0435\u0440\u0432\u0438\u0441",
"summary": "\U0001F6E0 \u0421\u043e\u0437\u0434\u0430\u043d \u043d\u043e\u0432\u044b\u0439 systemd \u0441\u0435\u0440\u0432\u0438\u0441",
"formatter": "crowdsec_service",
"skip_zero_firing": False,
},
"suspicious-outbound": {
"room": "security",
"title": "\U0001F310 CrowdSec: \u041f\u043e\u0434\u043e\u0437\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
"summary": "\U0001F310 \u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u043e\u0434\u043e\u0437\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
"formatter": "crowdsec_connection",
"skip_zero_firing": False,
},
}
```
**Теория:**
- Ключи словаря должны точно совпадать со значением `scenario` в JSON от CrowdSec
- `room: "security"` — комната Matrix для отправки
- `title` — заголовок алерта (используй Unicode escape для эмодзи)
- `formatter` — имя функции из `formatters.py` для форматирования тела сообщения
#### Шаг 4.5.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)
def crowdsec_mass_attack(alert_info):
lines = []
host = alert_info.get("host", "unknown")
value = alert_info.get("value", "")
lines.append(f"\U0001F5A5 Host: {host}")
lines.append(f"\U0001F4CA \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0431\u0430\u043d\u043e\u0432: {value}")
lines.append(f"\u26A1 \u0421\u0442\u0430\u0442\u0443\u0441: \u041a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f")
return "\n".join(lines)
def crowdsec_file_change(alert_info):
lines = []
host = alert_info.get("host", "unknown")
value = alert_info.get("value", "")
lines.append(f"\U0001F5A5 Host: {host}")
lines.append(f"\U0001F5C2\uFE0F \u0424\u0430\u0439\u043b: {value}")
return "\n".join(lines)
def crowdsec_package(alert_info):
lines = []
host = alert_info.get("host", "unknown")
value = alert_info.get("value", "")
lines.append(f"\U0001F5A5 Host: {host}")
lines.append(f"\U0001F9F0 \u041f\u0430\u043a\u0435\u0442: {value}")
return "\n".join(lines)
def crowdsec_service(alert_info):
lines = []
host = alert_info.get("host", "unknown")
value = alert_info.get("value", "")
lines.append(f"\U0001F5A5 Host: {host}")
lines.append(f"\U0001F6E0 \u0421\u0435\u0440\u0432\u0438\u0441: {value}")
return "\n".join(lines)
def crowdsec_connection(alert_info):
lines = []
host = alert_info.get("host", "unknown")
value = alert_info.get("value", "")
lines.append(f"\U0001F5A5 Host: {host}")
lines.append(f"\U0001F310 \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435: {value}")
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,
"crowdsec_mass_attack": crowdsec_mass_attack,
"crowdsec_file_change": crowdsec_file_change,
"crowdsec_package": crowdsec_package,
"crowdsec_service": crowdsec_service,
"crowdsec_connection": crowdsec_connection,
}
def get_formatter(name):
return FORMATTERS.get(name)
```
**Теория:**
- Каждая функция принимает `alert_info` — словарь с данными из JSON
- Возвращает строку с HTML-форматированием для Matrix
- Используй Unicode escape (`\U0001F5A5`) для эмодзи, чтобы избежать проблем с кодировкой
- Словарь `FORMATTERS` должен быть объявлен **после** всех функций, иначе Python выбросит `NameError`
#### Шаг 4.5.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.5.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.6. Настройка NPM Reverse Proxy на Gateway
#### Шаг 4.6.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.7. Тестирование интеграции
#### Шаг 4.7.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.7.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.7.3. Финальный тест всех сценариев
Создай скрипт `/tmp/final-test.sh` на VPS:
```bash
#!/bin/bash
echo "=== Запуск финального теста алертов ==="
echo "[1/7] Тест: Остановка сервиса (ufw)..."
systemctl stop ufw
sleep 3
systemctl start ufw
sleep 3
echo "[2/7] Тест: Изменение критического файла (/etc/crontab)..."
echo "# final test $(date)" >> /etc/crontab
sleep 3
echo "[3/7] Тест: Установка пакета (htop)..."
apt install -y --reinstall htop > /dev/null 2>&1
sleep 3
echo "[4/7] Тест: Новый systemd сервис..."
cat > /etc/systemd/system/test-final.service << 'EOF'
[Unit]
Description=Test Final
[Service]
ExecStart=/bin/true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
sleep 3
rm /etc/systemd/system/test-final.service
systemctl daemon-reload
sleep 3
echo "[5/7] Тест: Sudo эскалация..."
useradd -m -s /bin/bash testuser 2>/dev/null
echo "testuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
su - testuser -c "sudo ls /root" > /dev/null 2>&1
sleep 3
userdel -r testuser 2>/dev/null
sed -i '/testuser/d' /etc/sudoers
sleep 3
echo "[6/7] Тест: CrowdSec стандартный бан..."
> /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.55:12345: tls: client offered only unsupported versions" >> /var/log/3xui-access.log
done
sleep 5
echo "[7/7] Тест: CrowdSec массовая атака..."
> /var/log/3xui-access.log
for ip in {200..214}; do
for i in {1..5}; do
echo "$(date +'%Y/%m/%d %H:%M:%S') http: TLS handshake error from 198.51.100.$ip:12345: tls: client offered only unsupported versions" >> /var/log/3xui-access.log
done
done
sleep 5
echo "=== Тест завершён! Проверь Matrix ==="
```
```bash
chmod +x /tmp/final-test.sh
/tmp/final-test.sh
```
В Matrix должны прийти:
1. **⛔ Отключение защиты** — ufw
2. **🗂️ Изменение критического файла** — `/etc/crontab`
3. **📥 Установка пакета** — `apt install --reinstall htop`
4. **🛠 Новый systemd сервис** — `test-final.service`
5. **🗂️ Изменение критического файла** — `/etc/passwd` и `/etc/shadow` (от useradd/userdel)
6. **⚠️ Повышение привилегий** — `testuser` выполнил `sudo ls /root`
7. **🔥 TLS Errors** — бан IP `198.51.100.55`
8. **🚨 Массовая атака** — придёт через ~1 минуту (цикл mass-attack-monitor)
#### Шаг 4.7.4. Проверка логов
На 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://