:8086/` (или `https://`, если стоит реверс-прокси).
+## 4. Сценарий В: Интеграция CrowdSec (VPS мониторинг безопасности)
+
+CrowdSec — система обнаружения и блокировки атак. На VPS установлен CrowdSec, который автоматически банит IP за brute-force, CVE-эксплойты и другие атаки. Мы настроили отправку уведомлений в Matrix при каждом бане.
+
+### 4.1. Архитектура интеграции
+
+1. **CrowdSec** на VPS детектирует атаку и выносит решение (ban/alert)
+2. **Notification plugin** отправляет POST-запрос на `https://security.zailon.ru/`
+3. **NPM** на Gateway форвардит запрос на `192.168.1.208:8086` (manage-сервер)
+4. **matrix-relay** принимает запрос, парсит и отправляет в комнату `security` в Matrix
+
+### 4.2. Настройка CrowdSec на VPS
+
+#### Шаг 4.2.1. Создание notification config
+
+Создай файл `/etc/crowdsec/notifications/matrix-relay.yaml`:
+
+```yaml
+type: http
+name: matrix_relay
+method: POST
+format: |
+ {
+ "crowdsec_alerts": [
+ {{- range $index, $alert := . }}
+ {
+ "scenario": "{{ $alert.Scenario }}",
+ "value": "{{ $alert.Source.Value }}",
+ "scope": "{{ $alert.Source.Scope }}",
+ "action": "{{ if $alert.Remediation }}ban{{ else }}alert{{ end }}",
+ "reason": "{{ $alert.Message }}",
+ "host": "Charon-vpn"
+ }{{ if ne $index (sub (len $) 1) }},{{ end }}
+ {{- end }}
+ ]
+ }
+group_wait: 10s
+group_by:
+ - scenario
+ - source
+max_retry: 3
+timeout: 10s
+headers:
+ Content-Type: application/json
+url: https://security.zailon.ru/
+```
+
+**Теория:**
+- `type: http` — используем HTTP плагин для отправки
+- `method: POST` — обязательно POST, иначе relay вернёт 501
+- `format` — Go-шаблон, формирующий JSON для relay. Переменная `host` — имя сервера для отображения в алерте
+- `group_wait: 10s` — ждём 10 секунд, чтобы сгруппировать несколько событий в один алерт
+- `group_by` — группируем по сценарию и источнику
+- `url` — endpoint relay (через NPM reverse proxy)
+
+#### Шаг 4.2.2. Настройка profiles
+
+Открой `/etc/crowdsec/profiles.yaml` и добавь `notifications: - matrix_relay` в профили, которые выносят решения:
+
+```yaml
+name: default_ip_remediation
+filters:
+ - Alert.Remediation == true && Alert.GetScope() == "Ip"
+decisions:
+ - type: ban
+ duration: 4h
+notifications:
+ - matrix_relay
+on_success: break
+---
+name: default_range_remediation
+filters:
+ - Alert.Remediation == true && Alert.GetScope() == "Range"
+decisions:
+ - type: ban
+ duration: 4h
+notifications:
+ - matrix_relay
+on_success: break
+```
+
+**Теория:**
+- `notifications: - matrix_relay` — при срабатывании профиля CrowdSec вызовет notification plugin
+- `on_success: break` — после применения профиля прекращаем обработку (важно, иначе могут сработать другие профили)
+
+#### Шаг 4.2.3. Создание whitelist
+
+Создай файл `/etc/crowdsec/whitelist.txt` для IP, которые не должны триггерить алерты:
+
+```
+188.73.191.202
+2.27.50.20
+127.0.0.1
+```
+
+**Теория:**
+- Это whitelist для `security-monitor.sh` (см. ниже), не для CrowdSec
+- CrowdSec использует свои whitelist в `/etc/crowdsec/parsers/`
+
+### 4.3. Настройка security-monitor.sh на VPS
+
+CrowdSec отлично справляется с паттернами в логах (брутфорс, веб-атаки), но для системных событий (остановка сервисов, sudo) используем bash-скрипт, который мониторит логи и отправляет вебхуки напрямую в relay.
+
+#### Шаг 4.3.1. Создание скрипта
+
+Создай файл `/usr/local/bin/security-monitor.sh`:
+
+```bash
+#!/bin/bash
+
+RELAY_URL="https://security.zailon.ru/"
+HOST_NAME="Charon-vpn"
+FAILED_IPS_FILE="/tmp/failed_ssh_ips.txt"
+WHITELIST_FILE="/etc/crowdsec/whitelist.txt"
+STATE_FILE="/tmp/ssh-monitor-state.txt"
+
+send_alert() {
+ local scenario="$1"
+ local value="$2"
+ local scope="$3"
+ local reason="$4"
+
+ curl -s -X POST "$RELAY_URL" \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"crowdsec_alerts\": [{
+ \"scenario\": \"$scenario\",
+ \"value\": \"$value\",
+ \"scope\": \"$scope\",
+ \"action\": \"alert\",
+ \"reason\": \"$reason\",
+ \"host\": \"$HOST_NAME\"
+ }]
+ }" > /dev/null 2>&1
+}
+
+is_whitelisted() {
+ grep -qxF "$1" "$WHITELIST_FILE" 2>/dev/null
+}
+
+check_recent_login() {
+ local ip="$1"
+ local current_time=$(date +%s)
+
+ if [ -f "$STATE_FILE" ]; then
+ while IFS=' ' read -r stored_ip stored_time; do
+ if [ "$stored_ip" = "$ip" ]; then
+ local diff=$((current_time - stored_time))
+ if [ $diff -lt 30 ]; then
+ return 0
+ fi
+ fi
+ done < "$STATE_FILE"
+ fi
+
+ echo "$ip $current_time" >> "$STATE_FILE"
+ tail -20 "$STATE_FILE" > /tmp/state_tmp.txt && mv /tmp/state_tmp.txt "$STATE_FILE"
+ return 1
+}
+
+journalctl -u ssh.service -f --since now --no-pager | while read line; do
+ if echo "$line" | grep -q "Accepted"; then
+ IP=$(echo "$line" | grep -oP 'from \K[\d.]+')
+ USER=$(echo "$line" | grep -oP 'for \K\S+')
+
+ if check_recent_login "$IP"; then
+ continue
+ fi
+
+ if is_whitelisted "$IP"; then
+ continue
+ fi
+
+ if grep -q "$IP" "$FAILED_IPS_FILE" 2>/dev/null; then
+ REASON="SSH login by $USER (after failed attempts!)"
+ sed -i "/$IP/d" "$FAILED_IPS_FILE"
+ else
+ REASON="SSH login by $USER"
+ fi
+
+ send_alert "ssh-successful-login" "$IP" "Ip" "$REASON"
+ elif echo "$line" | grep -q "Failed password"; then
+ IP=$(echo "$line" | grep -oP 'from \K[\d.]+')
+ echo "$IP" >> "$FAILED_IPS_FILE"
+ [ $(wc -l < "$FAILED_IPS_FILE") -gt 1000 ] && > "$FAILED_IPS_FILE"
+ fi
+done &
+
+journalctl -u crowdsec.service -u ufw.service -u auditd.service -u docker-logs-converter.service -f --since now --no-pager | grep --line-buffered "Stopped" | while read line; do
+ SERVICE=$(echo "$line" | grep -oP 'Stopped \K\S+' | sed 's/\.service//')
+ [ -n "$SERVICE" ] && send_alert "service-disabled" "$SERVICE" "Service" "Protective service stopped"
+done &
+
+tail -F /var/log/auth.log 2>/dev/null | grep --line-buffered "sudo:" | grep --line-buffered "COMMAND=" | while read line; do
+ USER=$(echo "$line" | grep -oP 'sudo:\s+\K\S+')
+ CMD=$(echo "$line" | grep -oP 'COMMAND=\K.*')
+
+ [ "$USER" = "root" ] && continue
+
+ send_alert "sudo-escalation" "$USER" "User" "$CMD"
+done &
+
+wait
+```
+
+**Теория:**
+- `send_alert()` — функция отправки вебхука в relay с форматом `crowdsec_alerts`
+- `is_whitelisted()` — проверяет, есть ли IP в whitelist
+- `check_recent_login()` — дедупликация: если с того же IP было подключение в последние 30 секунд, игнорируем
+- Первый `journalctl` блок — мониторит SSH логины (успешные и неудачные)
+- Второй `journalctl` блок — мониторит остановку защитных сервисов (crowdsec, ufw, auditd, docker-logs-converter)
+- Третий `tail -F` блок — мониторит sudo команды от не-root пользователей
+- `--since now` — читаем только новые события, не историю
+- `--line-buffered` — для grep, чтобы обрабатывать строки сразу, не буферизируя
+
+#### Шаг 4.3.2. Создание systemd сервиса
+
+Создай файл `/etc/systemd/system/security-monitor.service`:
+
+```ini
+[Unit]
+Description=Security Events Monitor
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/security-monitor.sh
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+**Теория:**
+- `Restart=always` — автоматический перезапуск при падении
+- `RestartSec=10` — пауза 10 секунд перед перезапуском
+
+#### Шаг 4.3.3. Запуск сервиса
+
+```bash
+chmod +x /usr/local/bin/security-monitor.sh
+systemctl daemon-reload
+systemctl enable --now security-monitor
+systemctl status security-monitor --no-pager
+```
+
+### 4.4. Настройка relay на manage-сервере
+
+#### Шаг 4.4.1. Добавление сценариев в alerts.py
+
+Открой `/usr/local/bin/alerts/alerts.py` и добавь конфигурацию CrowdSec алертов:
+
+```python
+ALERTS_CONFIG = {
+ # ... существующие алерты ...
+
+ "3xui-tls-errors": {
+ "room": "security",
+ "title": "\U0001F525 CrowdSec: TLS Errors",
+ "summary": "\U0001F525 \u041c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 TLS handshake",
+ "info": "\U0001F6E0 \u041f\u0440\u043e\u0432\u0435\u0440\u044c \u043b\u043e\u0433\u0438 3x-ui \u043d\u0430 \u043f\u0440\u0435\u0434\u043c\u0435\u0442 \u0430\u0442\u0430\u043a",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+ },
+ "3xui-successful-login": {
+ "room": "security",
+ "title": "\u26A0\uFE0F CrowdSec: \u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0432\u0445\u043e\u0434 3x-ui",
+ "summary": "\U0001F464 \u0423\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u043f\u0430\u043d\u0435\u043b\u0438 3x-ui",
+ "formatter": "crowdsec_alert",
+ "skip_zero_firing": False,
+ },
+ "crowdsecurity/ssh-bf": {
+ "room": "security",
+ "title": "\U0001F525 CrowdSec: SSH Brute-Force",
+ "summary": "\U0001F511 \u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 SSH brute-force \u0430\u0442\u0430\u043a\u0430",
+ "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+ },
+ "crowdsecurity/ssh-slow-bf": {
+ "room": "security",
+ "title": "\U0001F525 CrowdSec: SSH Slow Brute-Force",
+ "summary": "\U0001F511 \u041c\u0435\u0434\u043b\u0435\u043d\u043d\u0430\u044f SSH brute-force \u0430\u0442\u0430\u043a\u0430",
+ "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+ },
+ "crowdsecurity/ssh-bf_user-enum": {
+ "room": "security",
+ "title": "\U0001F525 CrowdSec: SSH User Enumeration",
+ "summary": "\U0001F464 \u041f\u0435\u0440\u0435\u0431\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 SSH",
+ "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+ },
+ "crowdsecurity/http-bad-user-agent": {
+ "room": "security",
+ "title": "\U0001F525 CrowdSec: Bad User-Agent",
+ "summary": "\U0001F577\uFE0F \u041f\u043e\u0434\u043e\u0437\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 User-Agent",
+ "info": "\U0001F6E0 IP \u0437\u0430\u0431\u0430\u043d\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+ },
+ "ssh-successful-login": {
+ "room": "security",
+ "title": "\u2705 CrowdSec: SSH \u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0432\u0445\u043e\u0434",
+ "summary": "\U0001F511 \u0423\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e SSH",
+ "formatter": "crowdsec_alert",
+ "skip_zero_firing": False,
+ },
+ "service-disabled": {
+ "room": "security",
+ "title": "\u26D4 CrowdSec: \u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0438\u0442\u044b",
+ "summary": "\U0001F6A8 \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u0437\u0430\u0449\u0438\u0442\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430",
+ "formatter": "crowdsec_service_disabled",
+ "skip_zero_firing": False,
+ },
+ "sudo-escalation": {
+ "room": "security",
+ "title": "\u26A0\uFE0F CrowdSec: \u041f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0439",
+ "summary": "\U0001F511 \u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 root",
+ "formatter": "crowdsec_sudo",
+ "skip_zero_firing": False,
+ },
+}
+```
+
+**Теория:**
+- Ключи словаря должны точно совпадать со значением `scenario` в JSON от CrowdSec
+- `room: "security"` — комната Matrix для отправки
+- `title` — заголовок алерта (используй Unicode escape для эмодзи)
+- `formatter` — имя функции из `formatters.py` для форматирования тела сообщения
+
+#### Шаг 4.4.2. Добавление форматтеров в formatters.py
+
+Открой `/usr/local/bin/alerts/formatters.py` и добавь функции форматирования:
+
+```python
+def crowdsec_ban(alert_info):
+ lines = []
+ host = alert_info.get("host", "unknown")
+ value = alert_info.get("value", "")
+ scope = alert_info.get("scope", "")
+ lines.append(f"\U0001F5A5 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)
+
+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"{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.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(
+ "Webhook received
" +
+ json.dumps(obj, ensure_ascii=False, indent=2) +
+ "
"
+ )
+```
+
+**Теория:**
+- Проверка `"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://:8086/` (напрямую)
- **Метод:** POST
- **Content-Type:** `application/json`
### Пример вызова из bash-скрипта (для Docker, UPS, Cron):
+
```bash
-curl -X POST http://127.0.0.1:8086/ \
+curl -X POST https://security.zailon.ru/ \
-H "Content-Type: application/json" \
-d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}'
```
---
-## 5. Логирование и отладка
+## 6. Логирование и отладка
+
Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи:
+
```bash
journalctl -u matrix-relay -f
```
Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки:
-- `Processing Grafana alerts` — релей распознал формат Grafana.
-- `Processing backup notification` — релей распознал бэкап PVE/PBS.
-- `Unknown webhook type` — релей не нашел знакомых ключей и отправил "сырой" JSON в комнату `default`. Используй это для отладки новых источников!
\ No newline at end of file
+- `Processing CrowdSec alert` — релей распознал формат CrowdSec
+- `Processing Grafana alerts` — релей распознал формат Grafana
+- `Processing backup notification` — релей распознал бэкап PVE/PBS
+- `Unknown webhook type` — релей не нашел знакомых ключей и отправил "сырой" JSON в комнату `default`. Используй это для отладки новых источников!
+
+### Отладка CrowdSec на VPS:
+
+```bash
+tail -50 /var/log/crowdsec.log | grep -i "notif\|http\|matrix"
+```
+
+Типичные ошибки:
+- `HTTP server returned non 200 status code: 501` — relay не поддерживает GET, нужен `method: POST` в notification config
+- `can't evaluate field Env` — в шаблоне используется `$.Env.HOSTNAME`, которого нет. Используй жёстко заданное имя хоста
+- `notification 'matrix_relay' is defined multiple times` — дубликаты в `/etc/crowdsec/notifications/`, удали лишние
+
+### Отладка security-monitor.sh:
+
+```bash
+systemctl status security-monitor
+journalctl -u security-monitor -f
+```
+
+Для временной отладки добавь в скрипт:
+```bash
+DEBUG_LOG="/var/log/security-monitor-debug.log"
+echo "$(date): ALERT $scenario $value" >> "$DEBUG_LOG"
+```
+
+---
+
+## 7. Добавление новых сценариев CrowdSec
+
+### Шаг 7.1. Добавление нового сценария в alerts.py
+
+Открой `/usr/local/bin/alerts/alerts.py` и добавь новый ключ:
+
+```python
+"crowdsecurity/new-scenario": {
+ "room": "security",
+ "title": "\U0001F6A8 CrowdSec: New Scenario",
+ "summary": "\U0001F525 \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f",
+ "info": "\U0001F6E0 \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438",
+ "formatter": "crowdsec_ban",
+ "skip_zero_firing": False,
+},
+```
+
+**Теория:**
+- Имя ключа должно точно совпадать со значением `scenario` в JSON от CrowdSec
+- Можно посмотреть доступные сценарии: `cscli scenarios list` на VPS
+
+### Шаг 7.2. Перезапуск relay
+
+```bash
+systemctl restart matrix-relay
+```
+
+Новый сценарий подхватится автоматически, переписывать код не нужно.
+
+---
+
+## 8. Управление whitelist
+
+### Добавление IP в whitelist:
+
+```bash
+echo "1.2.3.4" >> /etc/crowdsec/whitelist.txt
+```
+
+### Просмотр whitelist:
+
+```bash
+cat /etc/crowdsec/whitelist.txt
+```
+
+**Теория:**
+- Whitelist используется только `security-monitor.sh` для фильтрации SSH-логинов
+- CrowdSec использует свои whitelist в `/etc/crowdsec/parsers/`
+- IP из whitelist не будут триггерить алерты об успешных SSH-входах
+
+---
+
+## 9. Ключевые файлы
+
+### VPS (Charon-vpn):
+- `/etc/crowdsec/notifications/matrix-relay.yaml` — notification config для CrowdSec
+- `/etc/crowdsec/profiles.yaml` — профили с notification
+- `/etc/crowdsec/whitelist.txt` — whitelist IP для security-monitor
+- `/usr/local/bin/security-monitor.sh` — скрипт мониторинга системных событий
+- `/etc/systemd/system/security-monitor.service` — systemd сервис
+
+### Manage (Home Server):
+- `/usr/local/bin/alerts/matrix-relay.py` — HTTP сервер relay
+- `/usr/local/bin/alerts/alerts.py` — конфигурация алертов
+- `/usr/local/bin/alerts/formatters.py` — форматтеры сообщений
+- `/usr/local/bin/alerts/crowdsec.py` — обработчик CrowdSec
+- `/etc/systemd/system/matrix-relay.service` — systemd сервис
+
+### Gateway (Olimp):
+- NPM Proxy Host: `security.zailon.ru` → `192.168.1.208:8086`
+
+---
+
+## 10. Решённые проблемы
+
+1. **`$.Env.HOSTNAME` в шаблоне CrowdSec** → жёстко заданное имя хоста `"host": "Charon-vpn"`
+2. **GET вместо POST в notification** → добавлен `method: POST`
+3. **Дублирование алертов** → дедупликация по времени (30 сек) в `security-monitor.sh`
+4. **CPU 100% от journalctl** → переключение на `tail -F` + правильный `journalctl --since now`
+5. **Старые события из журнала** → `--since now` + state file
+6. **Спам от своего IP** → whitelist `/etc/crowdsec/whitelist.txt`
+7. **Кириллица в Python файлах** → Unicode escape последовательности (`\U0001F5A5`)
+8. **Порядок проверок в relay** → `"crowdsec_alerts"` должен проверяться **перед** `"alerts"`
\ No newline at end of file