| .. | ||
| __init__.py | ||
| alerts.py | ||
| config.py | ||
| crowdsec.py | ||
| formatters.py | ||
| grafana.py | ||
| matrix-relay.py | ||
| matrix.py | ||
| pbs.py | ||
| README.md | ||
| rooms.py | ||
| utils.py | ||
Matrix Alert Relay — Документация
1. Архитектура и структура проекта
Проект представляет собой HTTP-сервер (вебхук-релей), который принимает POST-запросы от систем мониторинга, парсит их, форматирует и отправляет в нужные комнаты Matrix.
1.1. Общая схема
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 комнаты:
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:
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.
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).
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 добавь условие для определения нового источника и вызови свою функцию.
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. Архитектура интеграции
- CrowdSec на VPS детектирует атаку и выносит решение (ban/alert)
- Notification plugin отправляет POST-запрос на
https://security.zailon.ru/ - security-monitor.sh и mass-attack-monitor.sh отправляют вебхуки напрямую через curl
- NPM на Gateway форвардит запрос на
192.168.1.208:8086(manage-сервер) - matrix-relay принимает запрос, парсит и отправляет в комнату
securityв Matrix
4.2. Настройка CrowdSec на VPS
Шаг 4.2.1. Создание notification config
Создай файл /etc/crowdsec/notifications/matrix-relay.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 вернёт 501format— 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 в профили, которые выносят решения:
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 pluginon_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:
#!/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_alertsis_whitelisted()— проверяет, есть ли IP в whitelistcheck_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:
[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. Запуск сервиса
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:
#!/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:
[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. Запуск сервиса
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 алертов:
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 и добавь функции форматирования:
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:
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 и добавь импорт и обработку:
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
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
> /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:
#!/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 ==="
chmod +x /tmp/final-test.sh
/tmp/final-test.sh
В Matrix должны прийти:
- ⛔ Отключение защиты — ufw
- 🗂️ Изменение критического файла —
/etc/crontab - 📥 Установка пакета —
apt install --reinstall htop - 🛠 Новый systemd сервис —
test-final.service - 🗂️ Изменение критического файла —
/etc/passwdи/etc/shadow(от useradd/userdel) - ⚠️ Повышение привилегий —
testuserвыполнилsudo ls /root - 🔥 TLS Errors — бан IP
198.51.100.55 - 🚨 Массовая атака — придёт через ~1 минуту (цикл mass-attack-monitor)
Шаг 4.7.4. Проверка логов
На VPS:
tail -20 /var/log/crowdsec.log | grep -i "notif\|http\|matrix"
На manage:
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):
curl -X POST https://security.zailon.ru/ \
-H "Content-Type: application/json" \
-d '{"ups_name": "ServerRoom_UPS", "battery": 5, "status": "critical"}'
6. Логирование и отладка
Все события пишутся в стандартный вывод. Если релей запущен через systemd, смотри логи:
journalctl -u matrix-relay -f
Если алерт пришел, но не обработался так, как ожидалось, ищи в логах строки:
Processing CrowdSec alert— релей распознал формат CrowdSecProcessing Grafana alerts— релей распознал формат GrafanaProcessing backup notification— релей распознал бэкап PVE/PBSUnknown webhook type— релей не нашел знакомых ключей и отправил "сырой" JSON в комнатуdefault. Используй это для отладки новых источников!
Отладка CrowdSec на VPS:
tail -50 /var/log/crowdsec.log | grep -i "notif\|http\|matrix"
Типичные ошибки:
HTTP server returned non 200 status code: 501— relay не поддерживает GET, нуженmethod: POSTв notification configcan't evaluate field Env— в шаблоне используется$.Env.HOSTNAME, которого нет. Используй жёстко заданное имя хостаnotification 'matrix_relay' is defined multiple times— дубликаты в/etc/crowdsec/notifications/, удали лишние
Отладка security-monitor.sh:
systemctl status security-monitor
journalctl -u security-monitor -f
Для временной отладки добавь в скрипт:
DEBUG_LOG="/var/log/security-monitor-debug.log"
echo "$(date): ALERT $scenario $value" >> "$DEBUG_LOG"
Отладка mass-attack-monitor.sh:
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 и добавь новый ключ:
"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
systemctl restart matrix-relay
Новый сценарий подхватится автоматически, переписывать код не нужно.
8. Управление whitelist
Добавление IP в whitelist:
echo "1.2.3.4" >> /etc/crowdsec/whitelist.txt
Просмотр whitelist:
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. Решённые проблемы
$.Env.HOSTNAMEв шаблоне CrowdSec → жёстко заданное имя хоста"host": "Charon-vpn"- GET вместо POST в notification → добавлен
method: POST - Дублирование алертов SSH → дедупликация по времени (30 сек) в
security-monitor.sh - Дублирование алертов пакетов → дедупликация по времени (10 сек) через
PKG_STATE_FILE - CPU 100% от journalctl → переключение на
tail -F+ правильныйjournalctl --since now - Старые события из журнала →
--since now+ state file - Спам от своего IP → whitelist
/etc/crowdsec/whitelist.txt - Кириллица в Python файлах → Unicode escape последовательности (
\U0001F5A5) - Порядок проверок в relay →
"crowdsec_alerts"должен проверяться перед"alerts" - Дублирование systemd-сервисов →
inotifywait -e createбез-e modify NameErrorв formatters.py → словарьFORMATTERSобъявляется после всех функций- Битые unicode escape → пересоздание
alerts.pyцеликом приcat >>