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

1221 lines
51 KiB
Markdown
Raw Permalink 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<br/>• critical-file-change<br/>• package-installed<br/>• new-systemd-service<br/>• suspicious-outbound"]
MAM["mass-attack-monitor.sh<br/>━━━━━━━━━━━━<br/>• crowdsec/mass-attack<br/>(>50 банов за 5 мин)"]
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 (Olimp)"]
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
MAM -->|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 и 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"<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-эксплойты и другие атаки. Дополнительно работает 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 <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)
def crowdsec_mass_attack(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"\U0001F4CA <b>\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0431\u0430\u043d\u043e\u0432:</b> {value}")
lines.append(f"\u26A1 <b>\u0421\u0442\u0430\u0442\u0443\u0441:</b> \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 <b>Host:</b> {host}")
lines.append(f"\U0001F5C2\uFE0F <b>\u0424\u0430\u0439\u043b:</b> {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 <b>Host:</b> {host}")
lines.append(f"\U0001F9F0 <b>\u041f\u0430\u043a\u0435\u0442:</b> {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 <b>Host:</b> {host}")
lines.append(f"\U0001F6E0 <b>\u0421\u0435\u0440\u0432\u0438\u0441:</b> {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 <b>Host:</b> {host}")
lines.append(f"\U0001F310 <b>\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435:</b> {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"<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.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(
"<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.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://<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"
```
### Отладка mass-attack-monitor.sh:
```bash
systemctl status mass-attack-monitor
journalctl -u mass-attack-monitor -f
cscli decisions list --since 5m -o json | jq 'length'
cat /tmp/mass-attack-state.txt
```
---
## 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` — скрипт мониторинга системных событий
- `/usr/local/bin/mass-attack-monitor.sh` — скрипт мониторинга массовых атак
- `/etc/systemd/system/security-monitor.service` — systemd сервис
- `/etc/systemd/system/mass-attack-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. **Дублирование алертов SSH** → дедупликация по времени (30 сек) в `security-monitor.sh`
4. **Дублирование алертов пакетов** → дедупликация по времени (10 сек) через `PKG_STATE_FILE`
5. **CPU 100% от journalctl** → переключение на `tail -F` + правильный `journalctl --since now`
6. **Старые события из журнала**`--since now` + state file
7. **Спам от своего IP** → whitelist `/etc/crowdsec/whitelist.txt`
8. **Кириллица в Python файлах** → Unicode escape последовательности (`\U0001F5A5`)
9. **Порядок проверок в relay**`"crowdsec_alerts"` должен проверяться **перед** `"alerts"`
10. **Дублирование systemd-сервисов**`inotifywait -e create` без `-e modify`
11. **`NameError` в formatters.py** → словарь `FORMATTERS` объявляется **после** всех функций
12. **Битые unicode escape** → пересоздание `alerts.py` целиком при `cat >>`