From 06c8fe2e8e2da0a26736ac3654cc24b442d9d89e Mon Sep 17 00:00:00 2001 From: ssh-monitor Date: Tue, 14 Apr 2026 10:12:40 +1000 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=20SSH=20(?= =?UTF-8?q?journal=20+=20Accepted)=20=D0=B8=20logind=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20Service=3Dsshd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - journalctl: _COMM=sshd, fallback sshd/ssh; sort -u - dedupe Accepted в одном проходе по user|ip|port - LOGIND_SKIP_REMOTE: Type=ssh или Service=*sshd*, повторы loginctl --- README.md | 5 ++-- ssh-monitor | 54 ++++++++++++++++++++++++++++++++-------- ssh-monitor.conf.example | 2 +- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 73267b2..9e2d93b 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` - `LOGIND_NOTIFY_NEW` — `1` отправлять Telegram при появлении новой сессии в logind (строки вида *New session … of user …*). - `LOGIND_NOTIFY_REMOVED` — `1` отправлять Telegram при завершении сессии (*Removed session …*); по умолчанию `0` (только запись в `LOG_FILE`, чтобы не заспамить канал). - `LOGIND_NOTIFY_FAILED` — `1` отправлять Telegram по строкам logind, содержащим *failed* (широкий фильтр; при необходимости отключите). -- `LOGIND_SKIP_REMOTE` — `1` (по умолчанию): для новой сессии, если доступен `loginctl`, запрашивается тип сессии; при `Type=ssh` **второе** уведомление в Telegram не отправляется (успешный SSH уже покрывает `monitor_ssh`), в лог пишется пояснение. Поставьте `0`, если нужны отдельные алерты logind и для SSH-сессий. +- `LOGIND_SKIP_REMOTE` — `1` (по умолчанию): для новой сессии logind, если доступен `loginctl`, не отправляется второе Telegram, если сессия уже учтена в **`monitor_ssh`**: при **`Type=ssh`** или если **`Service`** указывает на **sshd** (часто **`Type=tty` + `Service=sshd`**); при пустом ответе `loginctl` делаются короткие повторы. Поставьте `0`, если нужны отдельные алерты logind и для SSH-сессий. - `DAILY_REPORT_HOUR` — час (0..23), после наступления которого в **текущих календарных сутках** (в выбранной ниже зоне) отправляется не более одного ежедневного отчёта. - `DAILY_REPORT_TZ` — необязательная **IANA**-зона (`Europe/Moscow`, `Asia/Yekaterinburg`, …). Если **пусто**, для отчёта используется та же зона, что и у команды `date` у процесса монитора (как правило, совпадает с `timedatectl` / `/etc/localtime` на сервере). Если сервис запускается с `TZ=UTC` в unit-файле, без `DAILY_REPORT_TZ` отчёт ориентируется на **UTC** — тогда задайте явную зону в конфиге. - `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа. @@ -104,7 +104,8 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` - Собираются сообщения юнита **`systemd-logind`** через **`journalctl`** (`-o cat`): новые и завершённые сессии, а также строки с подстрокой *failed* (если включено). - Без **`journalctl`** на хосте этот блок **не работает** (как и часть других функций, завязанных на journal). - Для разбора «новой сессии» используются типичные англоязычные форматы (`New session … of user …`). При несовпадении формата строка всё равно попадёт в лог с пометкой о неудачном разборе. -- **`loginctl`** нужен только для **`LOGIND_SKIP_REMOTE`**: определяется `Type` сессии; для удалённого SSH обычно `ssh`, чтобы не дублировать уведомление с блоком мониторинга SSH. +- **`loginctl`** нужен для **`LOGIND_SKIP_REMOTE`**: по `Type` и **`Service`** решается, не дублировать ли Telegram с **`monitor_ssh`** (см. описание `LOGIND_SKIP_REMOTE` выше). +- **`monitor_ssh`**: события **sshd** читаются из journal преимущественно как **`journalctl _COMM=sshd`** (запасные варианты — юниты `sshd` / `ssh`), строки **`sort -u`**, а для **`Accepted`** в одном проходе цикла подавляется повтор с тем же **пользователем, IP и номером порта клиента** — чтобы не было двух одинаковых «успешных SSH» из дублирующихся записей журнала. Команда **`ssh-monitor --check-config`** выводит актуальные значения, в том числе параметры logind. diff --git a/ssh-monitor b/ssh-monitor index 878d182..c318d0d 100644 --- a/ssh-monitor +++ b/ssh-monitor @@ -54,7 +54,7 @@ ENABLE_LOGIND_MONITOR=1 LOGIND_NOTIFY_NEW=1 LOGIND_NOTIFY_REMOVED=0 LOGIND_NOTIFY_FAILED=1 -# 1 = не слать Telegram для новых сессий с типом ssh (уведомление уже идёт из monitor_ssh) +# 1 = не слать дубль Telegram для SSH-сессий logind (Type=ssh или Service=*sshd*; см. logind_session_is_ssh_duplicate_for_notify) LOGIND_SKIP_REMOTE=1 # Эксплуатация / health @@ -952,11 +952,19 @@ monitor_ssh() { local ssh_logs="" if command -v journalctl >/dev/null 2>&1; then - ssh_logs=$(journalctl -u ssh -u sshd --since="@${last_check}" --until="now" 2>/dev/null | grep -E "Accepted|Failed|Disconnected" || true) + # -u ssh и -u sshd вместе часто дают две одинаковые строки Accepted; _COMM=sshd — один поток. + ssh_logs=$(journalctl _COMM=sshd --since="@${last_check}" --until="now" 2>/dev/null | grep -E "Accepted|Failed|Disconnected" | sort -u || true) + if [ -z "$ssh_logs" ]; then + ssh_logs=$(journalctl -u sshd --since="@${last_check}" --until="now" 2>/dev/null | grep -E "Accepted|Failed|Disconnected" | sort -u || true) + fi + if [ -z "$ssh_logs" ]; then + ssh_logs=$(journalctl -u ssh --since="@${last_check}" --until="now" 2>/dev/null | grep -E "Accepted|Failed|Disconnected" | sort -u || true) + fi else - ssh_logs=$(grep -E "sshd.*(Accepted|Failed|Disconnected)" /var/log/auth.log 2>/dev/null || true) + ssh_logs=$(grep -E "sshd.*(Accepted|Failed|Disconnected)" /var/log/auth.log 2>/dev/null | sort -u || true) fi + declare -A SSH_ACCEPT_NOTIFY_KEYS if [ -n "$ssh_logs" ]; then while IFS= read -r line; do [[ -z "$line" ]] && continue @@ -965,6 +973,14 @@ monitor_ssh() { user=$(sed -nE 's/.* for ([^ ]+) from .*/\1/p' <<<"$line") ip="$(extract_ssh_client_ip "$line")" if [ -n "$user" ]; then + local dedupe_port dedupe_key + dedupe_port=$(sed -nE 's/.* from [^ ]+ port ([0-9]+).*/\1/p' <<<"$line") + dedupe_key="${user}|${ip:-}|${dedupe_port:-}" + if [[ -n "${SSH_ACCEPT_NOTIFY_KEYS[$dedupe_key]:-}" ]]; then + write_log "SSH Accepted: дубль в журнале (user=${user} ip=${ip:-} port=${dedupe_port:-?}), второе уведомление не отправляем" + continue + fi + SSH_ACCEPT_NOTIFY_KEYS[$dedupe_key]=1 local message=" ✅ УСПЕШНОЕ SSH ПОДКЛЮЧЕНИЕ "$'\n' message="${message}👤 Пользователь: ${user}"$'\n' message="${message}🌐 IP адрес: ${ip:-local}"$'\n' @@ -1196,14 +1212,32 @@ monitor_security_events() { # Локальные сессии (systemd-logind, journalctl) # ============================================ -logind_session_type() { +# Возврат 0 = не слать Telegram по logind (дубль с monitor_ssh): Type=ssh или Service=*sshd*. +# Пустой ответ loginctl — короткие повторы (гонка с journal). +logind_session_is_ssh_duplicate_for_notify() { local sid="$1" - [ -z "$sid" ] && echo "" && return 0 + [ -z "$sid" ] && return 1 if ! command -v loginctl >/dev/null 2>&1; then - echo "" + return 1 + fi + local attempt st svc svc_lc + for attempt in 1 2 3 4 5; do + local props + props=$(loginctl show-session "$sid" -p Type -p Service 2>/dev/null || true) + st=$(printf '%s\n' "$props" | sed -n 's/^Type=//p' | head -n1 | tr -d '[:space:]') + svc=$(printf '%s\n' "$props" | sed -n 's/^Service=//p' | head -n1 | tr -d '[:space:]') + svc_lc=$(printf '%s' "$svc" | tr '[:upper:]' '[:lower:]') + if [ "$st" = "ssh" ] || [[ "$svc_lc" == *sshd* ]]; then return 0 fi - loginctl show-session "$sid" -p Type 2>/dev/null | sed -n 's/^Type=//p' | tr -d '[:space:]' + if [ -n "$st" ] || [ -n "$svc" ]; then + return 1 + fi + if [ "$attempt" -lt 5 ]; then + sleep 0.2 + fi + done + return 1 } parse_logind_new_session() { @@ -1265,10 +1299,8 @@ monitor_logind() { local sid="" user="" if parse_logind_new_session "$line" sid user; then if [ "${LOGIND_SKIP_REMOTE:-1}" = "1" ] && [ -n "$sid" ]; then - local st - st=$(logind_session_type "$sid") - if [ "$st" = "ssh" ]; then - write_log "LOGIND: новая сессия (тип ssh, уведомление от ssh-monitor): ${line}" + if logind_session_is_ssh_duplicate_for_notify "$sid"; then + write_log "LOGIND: новая сессия (SSH уже в monitor_ssh, Telegram пропущен): ${line}" continue fi fi diff --git a/ssh-monitor.conf.example b/ssh-monitor.conf.example index 4d388ee..1037757 100644 --- a/ssh-monitor.conf.example +++ b/ssh-monitor.conf.example @@ -54,7 +54,7 @@ ENABLE_LOGIND_MONITOR="1" LOGIND_NOTIFY_NEW="1" LOGIND_NOTIFY_REMOVED="0" LOGIND_NOTIFY_FAILED="1" -# 1 = не дублировать Telegram для новых сессий с типом ssh (уведомление уже есть из SSH-монитора) +# 1 = не дублировать Telegram для SSH-сессий logind (Type=ssh или Service=*sshd*; см. README) LOGIND_SKIP_REMOTE="1" DAILY_REPORT_HOUR="9"