Обновить docs/services/lxc208-manage/alert/README.md
This commit is contained in:
parent
6381c44ef7
commit
6e69b9afc9
@ -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"`
|
||||||
Loading…
Reference in New Issue
Block a user