From b801cf2385892f790f43ca9406493642921e77a0 Mon Sep 17 00:00:00 2001 From: PTah Date: Mon, 25 May 2026 09:23:32 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D0=B2?= =?UTF-8?q?=D0=BE=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=83=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D1=85=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20message=5Fensure=5Fserver=5Fline=20=D0=B8=20SERVER=5FD?= =?UTF-8?q?ISPLAY=5FNAME;=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=201.1.3-s?= =?UTF-8?q?erver-label.=20=D0=93=D0=B0=D0=B9=D0=B4=D1=8B=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=D0=BC=20=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8E.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++- docs/auto-update.ru.md | 126 +++++++++++++++++++++++++++++++ docs/install-prerequisites.ru.md | 5 ++ docs/notifications.ru.md | 98 ++++++++++++++++++++++++ readme_eng.md | 20 ++++- ssh-monitor | 81 +++++++++++++------- ssh-monitor.conf.example | 2 + 7 files changed, 321 insertions(+), 31 deletions(-) create mode 100644 docs/auto-update.ru.md create mode 100644 docs/notifications.ru.md diff --git a/README.md b/README.md index 98e5a6c..f2a1f48 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` |------|------------| | **`ssh-monitor`** | Основной цикл: разбор журналов (**journalctl**: sshd, sudo, «тяжёлые» события, **systemd-logind**), цепочка уведомлений (Telegram, SMTP через **python3**, резервный webhook), при **`ENABLE_IP_BAN=1`** — учёт неудачных попыток, бан в **ipset** и согласование с **iptables**/**ip6tables**, ежедневный отчёт, heartbeat, опционально **Prometheus** textfile и JSON health. Режимы: **`--check-config`**, **`--dry-run`**. | | **`ssh-monitor-watchdog`** | Отдельный скрипт (обычно по **systemd timer**): проверяет, что сервис монитора активен и файл heartbeat не старше порога; при сбое выполняет **`systemctl restart`** и шлёт уведомление в Telegram или в **`BACKUP_WEBHOOK_URL`**. Читает те же `TELEGRAM_*` / webhook из `/etc/ssh-monitor.conf`. | +| **`update_ssh_monitor.sh`** | Опциональное автообновление: `git pull` / `clone`, сравнение SHA256 с `/usr/local/bin/ssh-monitor`, перезапуск `ssh-monitor.service`. См. [docs/auto-update.ru.md](docs/auto-update.ru.md). | **Системные пакеты и зависимости** (что поставить на сервер до запуска): пошагово в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md). @@ -33,7 +34,7 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` - `BACKUP_WEBHOOK_URL` — резервная доставка JSON `{"text":"..."}` (например Slack Incoming Webhook), если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить сообщение; **не заменяет** обязательность хотя бы одного основного канала (см. шаг 3) - `LOG_FILE`, `LAST_HEARTBEAT_FILE`, `LAST_REPORT_FILE`, `LAST_SSH_CHECK_FILE`, `LAST_SUDO_CHECK_FILE`, `LAST_SECURITY_EVENTS_FILE`, `LAST_LOGIND_CHECK_FILE`, `BAN_LIST_FILE` - `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — см. раздел «Мониторинг systemd-logind» ниже -- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` +- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SERVER_DISPLAY_NAME` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` - `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC` - `PROMETHEUS_TEXTFILE_DIR` — каталог для `ssh_monitor.prom` (совместимость с node_exporter textfile collector) - `HEALTHCHECK_STATUS_FILE` — путь к JSON-файлу с меткой последней итерации цикла @@ -67,6 +68,7 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` - `DAILY_REPORT_HOUR` — час (0..23), после наступления которого в **текущих календарных сутках** (в выбранной ниже зоне) отправляется не более одного ежедневного отчёта. - `DAILY_REPORT_TZ` — необязательная **IANA**-зона (`Europe/Moscow`, `Asia/Yekaterinburg`, …). Если **пусто**, для отчёта используется та же зона, что и у команды `date` у процесса монитора (как правило, совпадает с `timedatectl` / `/etc/localtime` на сервере). Если сервис запускается с `TZ=UTC` в unit-файле, без `DAILY_REPORT_TZ` отчёт ориентируется на **UTC** — тогда задайте явную зону в конфиге. - `NOTIFY_TZ` — **IANA**-зона для строк **«🕐 Время»** в Telegram/email (и аналогичных текстах). **Пусто** — используется **`DAILY_REPORT_TZ`**, если она задана, иначе зона процесса. Частая причина «время в теле сообщения UTC, а в заголовке чата локальное»: в **`ssh-monitor.service`** задано **`Environment=TZ=UTC`** — тогда задайте **`NOTIFY_TZ`** (или **`DAILY_REPORT_TZ`**) на вашу локаль, например `Asia/Vladivostok`. +- `SERVER_DISPLAY_NAME` — подпись в строке **«🖥️ Сервер»** во всех уведомлениях. **Пусто** — `hostname` и при возможности основной IPv4 (`hostname (10.x.x.x)`). Задайте явное имя, если несколько хостов с одинаковым hostname шлют в один чат. Подробнее: [docs/notifications.ru.md](docs/notifications.ru.md). - `SSH_ACCEPT_NOTIFY_DEDUP_SEC` — не чаще одного Telegram по **успешному SSH (`Accepted`)** на одну пару **пользователь + IP** за указанное число **секунд** (по умолчанию **5**). Снимает дубли, когда в journal две строки с **разными портами клиента** за один вход. **`0`** — отключить этот антидубль (останется только дедупликация внутри одного прохода `monitor_ssh` по `user|ip|port`). - `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа. - `BRUTE_WINDOW_SEC` — окно (секунды) для оценки «массового» брутфорса по `journalctl`. @@ -109,6 +111,20 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **` Ограничения на хостах **без journald** и нюансы **`/var/log/auth.log`**: см. раздел 4 в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md). +### Идентификация сервера в уведомлениях + +Во **всех** оповещениях (SSH, sudo, бан IP, logind, heartbeat, ежедневный отчёт и т.д.) после заголовка автоматически добавляется строка **«🖥️ Сервер: …»** — формат `hostname (IPv4)` или только `hostname`, если IPv4 определить не удалось. Логика в `notify_send()`; дублирование в тексте сообщения не требуется. + +- Утилиты **`hostname`** и **`ip`** (см. [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md)). +- Опционально **`SERVER_DISPLAY_NAME`** в конфиге — фиксированная подпись вместо hostname (удобно при нескольких серверах в одном Telegram-чате). +- Типы сообщений и примеры: [docs/notifications.ru.md](docs/notifications.ru.md). + +### Автообновление скрипта + +Скрипт **`update_ssh_monitor.sh`** по расписанию (cron или systemd timer) выполняет `git pull` / `clone`, сравнивает SHA256 с `/usr/local/bin/ssh-monitor` и при отличии копирует новую версию и перезапускает **`ssh-monitor.service`**. Конфиг **`/etc/ssh-monitor.conf`** не трогается. + +Пошаговая настройка: [docs/auto-update.ru.md](docs/auto-update.ru.md). + ## Режимы запуска Обычный запуск (root): @@ -137,7 +153,7 @@ bash -n ./ssh-monitor ## Релизный архив -Версия задаётся в скрипте переменной **`SSH_MONITOR_VERSION`** (на момент последнего обновления документации — **1.1.2-ipset**; актуальное значение смотрите в файле **`ssh-monitor`**). Сборка tarball из текущего git-дерева: +Версия задаётся в скрипте переменной **`SSH_MONITOR_VERSION`** (на момент последнего обновления документации — **1.1.3-server-label**; актуальное значение смотрите в файле **`ssh-monitor`**). Сборка tarball из текущего git-дерева: ```bash make dist diff --git a/docs/auto-update.ru.md b/docs/auto-update.ru.md new file mode 100644 index 0000000..a92aa80 --- /dev/null +++ b/docs/auto-update.ru.md @@ -0,0 +1,126 @@ +# Автообновление ssh-monitor + +Скрипт **`update_ssh_monitor.sh`** в корне репозитория обновляет установленный **`/usr/local/bin/ssh-monitor`** из git-репозитория и перезапускает systemd-сервис. Конфиг **`/etc/ssh-monitor.conf`**, watchdog и таймеры **не изменяются**. + +Установка самого монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md). + +## Что делает скрипт + +1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`). +2. Проверяет наличие **`ipset`**; при отсутствии пытается установить через пакетный менеджер (apt, yum, dnf, pacman, zypper). Без ipset скрипт завершается с ошибкой — новая версия монитора с **`ENABLE_IP_BAN=1`** требует ipset. +3. В **`UPDATE_DIR`** выполняет **`git clone`** или **`git pull origin main`** репозитория. +4. Ищет файл **`ssh-monitor`** в клоне: + - `$UPDATE_DIR/ssh-monitor/src/branch/main/ssh-monitor` (структура GitLab); + - `$UPDATE_DIR/ssh-monitor/ssh-monitor` (плоский репозиторий, как в этом проекте). +5. Сравнивает **SHA256** удалённого файла с **`LOCAL_SCRIPT_PATH`** (по умолчанию `/usr/local/bin/ssh-monitor`). +6. При отличии: + - резервная копия `ssh-monitor.backup.YYYYMMDD_HHMMSS`; + - `cp` + `chmod +x`; + - **`systemctl restart ssh-monitor.service`** (если есть systemctl). +7. Пишет лог в **`LOG_FILE`** (по умолчанию `/var/log/update_script.log`). + +```mermaid +flowchart LR + A[cron/timer] --> B[update_ssh_monitor.sh] + B --> C[git pull/clone] + C --> D{SHA256 различается?} + D -->|да| E[backup + cp] + E --> F[restart ssh-monitor.service] + D -->|нет| G[без изменений] +``` + +## Настройка перед первым запуском + +Отредактируйте переменные в начале **`update_ssh_monitor.sh`**: + +| Переменная | По умолчанию | Назначение | +|------------|--------------|------------| +| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона | +| `REPO_URL` | `https://GIT/user/ssh-monitor.git` | **Замените** на ваш реальный URL | +| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать бинарник | +| `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений | +| `DEBUG` | `0` | `1` — подробный DEBUG в лог | + +Требования на сервере: + +- **root** (или sudo) для `cp`, `systemctl`, установки ipset; +- **git** и сетевой доступ к репозиторию (SSH-ключ или HTTPS); +- ветка в скрипте жёстко **`main`** — при другой ветке измените `git pull` в файле. + +## Установка + +```bash +sudo mkdir -p /opt/scripts +sudo cp update_ssh_monitor.sh /opt/scripts/update_ssh_monitor.sh +sudo chmod 750 /opt/scripts/update_ssh_monitor.sh +``` + +Проверка вручную: + +```bash +sudo /opt/scripts/update_ssh_monitor.sh +sudo tail -20 /var/log/update_script.log +sudo systemctl status ssh-monitor +``` + +## Запуск по расписанию + +### Cron (пример: каждый день в 04:00) + +```bash +sudo crontab -e +``` + +Добавьте: + +``` +0 4 * * * /opt/scripts/update_ssh_monitor.sh +``` + +### systemd timer (альтернатива) + +Создайте `/etc/systemd/system/ssh-monitor-update.service`: + +```ini +[Unit] +Description=Update ssh-monitor from git + +[Service] +Type=oneshot +ExecStart=/opt/scripts/update_ssh_monitor.sh +``` + +И `/etc/systemd/system/ssh-monitor-update.timer`: + +```ini +[Unit] +Description=Daily ssh-monitor update + +[Timer] +OnCalendar=*-*-* 04:00:00 +Persistent=true + +[Install] +WantedBy=timers.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now ssh-monitor-update.timer +``` + +## Откат после неудачного обновления + +Резервные копии лежат рядом с установленным скриптом: + +```bash +ls -la /usr/local/bin/ssh-monitor.backup.* +sudo cp /usr/local/bin/ssh-monitor.backup.YYYYMMDD_HHMMSS /usr/local/bin/ssh-monitor +sudo systemctl restart ssh-monitor.service +``` + +## Ограничения + +- Обновляется **только** файл **`ssh-monitor`**, не конфиг и не watchdog. +- При недоступном git **`git pull`** пишет предупреждение и продолжает с локальной копией. +- Placeholder **`REPO_URL`** в репозитории нужно заменить на боевой URL до использования в продакшене. diff --git a/docs/install-prerequisites.ru.md b/docs/install-prerequisites.ru.md index 66230aa..4dec713 100644 --- a/docs/install-prerequisites.ru.md +++ b/docs/install-prerequisites.ru.md @@ -145,3 +145,8 @@ sudo journalctl -u ssh-monitor-watchdog.service -f - [ ] (По желанию) настроено сохранение правил после перезагрузки. Далее: установка самого **`ssh-monitor`**, unit **systemd** и проверка — в [README.md](../README.md). + +Дополнительно: + +- [notifications.ru.md](notifications.ru.md) — подпись **«🖥️ Сервер»** в уведомлениях, типы алертов, часовые пояса. +- [auto-update.ru.md](auto-update.ru.md) — автообновление скрипта через **`update_ssh_monitor.sh`**. diff --git a/docs/notifications.ru.md b/docs/notifications.ru.md new file mode 100644 index 0000000..91e8990 --- /dev/null +++ b/docs/notifications.ru.md @@ -0,0 +1,98 @@ +# Уведомления ssh-monitor + +Документ описывает, **какие сообщения** отправляет монитор, **какие поля** в них есть и как настроить **подпись сервера** и **часовой пояс**. + +См. также: [README.md](../README.md), пример конфига [ssh-monitor.conf.example](../ssh-monitor.conf.example). + +## Подпись сервера (🖥️ Сервер) + +Начиная с версии **1.1.3-server-label**, каждое уведомление, проходящее через **`notify_send()`**, получает строку **«🖥️ Сервер: …»** сразу **после первой строки** (заголовка алерта). + +Формат значения: + +| Условие | Пример | +|---------|--------| +| `SERVER_DISPLAY_NAME` пусто, IPv4 найден | `web01 (10.1.20.5)` | +| `SERVER_DISPLAY_NAME` пусто, IPv4 нет | `web01` | +| Задан `SERVER_DISPLAY_NAME="prod-db-01"` | `prod-db-01 (10.1.20.5)` | + +IPv4 определяется через `ip -4 route get` (исходящий адрес) или `hostname -I`. Нужны утилиты **`hostname`** и **`ip`** — см. [install-prerequisites.ru.md](install-prerequisites.ru.md). + +Если в тексте сообщения уже есть подстрока **`🖥️ Сервер:`**, вторая строка **не добавляется** (защита от дубля при ручной вставке). + +### Пример: успешный SSH + +``` +✅ УСПЕШНОЕ SSH ПОДКЛЮЧЕНИЕ +🖥️ Сервер: myhost (10.1.20.5) +👤 Пользователь: alice +🌐 IP адрес: 10.1.20.1 +🕐 Время: 25.05.2026 09:08:22 +``` + +### Пример: sudo + +``` +⚠️ ИСПОЛЬЗОВАНИЕ SUDO +🖥️ Сервер: myhost (10.1.20.5) +👤 Пользователь: alice +🔑 От имени USER: root +💻 Команда: /bin/bash +📁 Директория: /home/alice +🕐 Время: 24.05.2026 16:54:49 +``` + +## Настройка в `/etc/ssh-monitor.conf` + +```bash +# Пусто = hostname (+ IPv4 при возможности) +SERVER_DISPLAY_NAME="" + +# Или фиксированное имя для этого хоста в общем чате: +# SERVER_DISPLAY_NAME="dc1-app-03" +``` + +Проверка эффективной подписи: + +```bash +sudo ssh-monitor --check-config +``` + +В выводе будет строка `SERVER_DISPLAY_NAME=... (эффективно: ...)`. + +## Типы уведомлений + +| Событие | Заголовок (первая строка) | Доп. поля | +|---------|----------------------------|-----------| +| Успешный SSH | ✅ УСПЕШНОЕ SSH ПОДКЛЮЧЕНИЕ | пользователь, IP клиента, время | +| SSH под root | 🔑 SSH ВХОД ПОД ROOT | IP, время | +| Неудачная попытка SSH | ❌ НЕУДАЧНАЯ ПОПЫТКА SSH | пользователь, IP, счётчик попыток, время | +| Sudo | ⚠️ ИСПОЛЬЗОВАНИЕ SUDO | пользователь, USER=, команда, PWD, время | +| Бан IP | 🚫 IP ЗАБЛОКИРОВАН АВТОМАТИЧЕСКИ | IP, попытки, длительность, время | +| Порог без бана | ⚠️ ЛИМИТ НЕУДАЧНЫХ SSH (автобан отключён…) | IP, попытки, время | +| Брутфорс | 🧨 ВОЗМОЖНЫЙ МАССОВЫЙ БРУТФОРС SSH | IP, счётчик за окно, время | +| Host key / MITM | 🔐 ВНИМАНИЕ: изменение SSH host key… | фрагмент журнала, время | +| logind: новая сессия | 🖥️ НОВАЯ СЕССИЯ (systemd-logind) | пользователь, ID сессии, время | +| logind: сбой | ❌ СБОЙ (systemd-logind) | строка журнала, время | +| Ежедневный отчёт | 📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА | статистика 24 ч, баны, топ IP, сессии | +| Heartbeat | ❤️ Heartbeat - скрипт мониторинга работает | время | +| Старт / стоп | ✅ СКРИПТ МОНИТОРИНГА ЗАПУЩЕН / ⚠️ ОСТАНОВЛЕН | версия, каналы, время | + +Во всех перечисленных случаях **🖥️ Сервер** добавляется автоматически (кроме сообщений, где вы сами вставили эту строку). + +## Время в сообщениях (🕐) + +Строки **«🕐 Время»** формируются функцией **`notification_date()`**: + +1. Если задан **`NOTIFY_TZ`** (IANA, например `Europe/Moscow`) — используется он. +2. Иначе, если задан **`DAILY_REPORT_TZ`** — он. +3. Иначе — зона процесса (`date` у systemd-сервиса; часто UTC, если в unit указано `Environment=TZ=UTC`). + +Ежедневный отчёт по календарю и часу **`DAILY_REPORT_HOUR`** использует **`DAILY_REPORT_TZ`** (или зону процесса) — см. README. + +## Каналы доставки + +- **`NOTIFY_CHAIN`**: `telegram`, `email` — при каждом событии попытка **во все** каналы списка. +- **`BACKUP_WEBHOOK_URL`**: только если **все** каналы цепочки не доставили сообщение. + +Режим **`--dry-run`**: уведомления не отправляются; в stderr печатается текст **уже с подписью сервера**. diff --git a/readme_eng.md b/readme_eng.md index b565637..96f1413 100644 --- a/readme_eng.md +++ b/readme_eng.md @@ -8,6 +8,7 @@ Bash script that monitors **SSH**, **SUDO**, and **`systemd-logind`** events (lo |------|---------| | **`ssh-monitor`** | Main loop: parses logs (**journalctl**: sshd, sudo, security events, **systemd-logind**), notification chain (Telegram, SMTP via **python3**, backup webhook); with **`ENABLE_IP_BAN=1`** tracks failed attempts, bans via **ipset**, reconciles with **iptables**/**ip6tables**, daily report, heartbeat, optional **Prometheus** textfile and JSON health. Modes: **`--check-config`**, **`--dry-run`**. | | **`ssh-monitor-watchdog`** | Standalone script (usually via **systemd timer**): checks that the monitor service is active and the heartbeat file is fresh; on failure runs **`systemctl restart`** and sends Telegram or **`BACKUP_WEBHOOK_URL`**. Reads the same `TELEGRAM_*` / webhook from `/etc/ssh-monitor.conf`. | +| **`update_ssh_monitor.sh`** | Optional auto-update: `git pull` / `clone`, SHA256 compare with `/usr/local/bin/ssh-monitor`, restart `ssh-monitor.service`. See [docs/auto-update.ru.md](docs/auto-update.ru.md) (Russian). | **System packages and dependencies** (install on the server before running): step-by-step in [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md) (Russian). @@ -33,7 +34,7 @@ Supported parameters: - `BACKUP_WEBHOOK_URL` — fallback JSON `{"text":"..."}` (e.g. Slack Incoming Webhook) if **all** **`NOTIFY_CHAIN`** channels fail; **does not** replace the need for at least one primary channel (see step 3) - `LOG_FILE`, `LAST_HEARTBEAT_FILE`, `LAST_REPORT_FILE`, `LAST_SSH_CHECK_FILE`, `LAST_SUDO_CHECK_FILE`, `LAST_SECURITY_EVENTS_FILE`, `LAST_LOGIND_CHECK_FILE`, `BAN_LIST_FILE` - `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — see **«systemd-logind monitoring»** below -- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (optional), `NOTIFY_TZ` (optional), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` +- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (optional), `NOTIFY_TZ` (optional), `SERVER_DISPLAY_NAME` (optional), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` - `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC` - `PROMETHEUS_TEXTFILE_DIR` — directory for `ssh_monitor.prom` (node_exporter textfile collector) - `HEALTHCHECK_STATUS_FILE` — path to JSON with last loop iteration marker @@ -67,6 +68,7 @@ If `/etc/ssh-monitor.conf` is missing or some values are empty, script defaults - `DAILY_REPORT_HOUR` — hour (0..23) after which at most one daily report is sent in the **current calendar day** (in the zone below). - `DAILY_REPORT_TZ` — optional **IANA** zone (`Europe/Moscow`, …). If **empty**, report uses the monitor process zone (usually matches `timedatectl` / `/etc/localtime`). If the unit sets `TZ=UTC`, without `DAILY_REPORT_TZ` the report uses **UTC** — set an explicit zone in config. - `NOTIFY_TZ` — **IANA** zone for **«🕐 Время»** lines in Telegram/email. **Empty** — use **`DAILY_REPORT_TZ`** if set, else process zone. Common mismatch: **`ssh-monitor.service`** has **`Environment=TZ=UTC`** — set **`NOTIFY_TZ`** or **`DAILY_REPORT_TZ`** to your locale, e.g. `Asia/Vladivostok`. +- `SERVER_DISPLAY_NAME` — label in **«🖥️ Server»** line on every alert. **Empty** — `hostname` plus primary IPv4 when available. Set a fixed name when many hosts share one Telegram chat. Details: [docs/notifications.ru.md](docs/notifications.ru.md) (Russian). - `SSH_ACCEPT_NOTIFY_DEDUP_SEC` — at most one Telegram per successful SSH (**Accepted**) per **user + IP** pair within **N** seconds (default **5**). Removes duplicates when journal has two lines with **different client ports** for one login. **`0`** disables this cross-iteration dedup (in-pass dedup by `user|ip|port` remains). - `DAILY_REPORT_TOP_IPS` — how many IPs in the failed-attempts top for 24 hours. - `BRUTE_WINDOW_SEC` — window (seconds) for “mass” brute-force evaluation via `journalctl`. @@ -109,6 +111,20 @@ If **`NOTIFY_CHAIN` is empty**, the script exits with **`Не настроен Hosts **without journald** and **`/var/log/auth.log`** caveats: section 4 in [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md). +### Server identification in alerts + +**All** notifications sent via **`notify_send()`** get a **«🖥️ Сервер: …»** line (Russian label in messages) right after the title: `hostname (IPv4)` or hostname only. Implemented centrally in **`message_ensure_server_line()`**; no need to add the line in each alert builder. + +- Requires **`hostname`** and **`ip`** (see prerequisites doc). +- Optional **`SERVER_DISPLAY_NAME`** in config for a fixed label. +- Message types and examples: [docs/notifications.ru.md](docs/notifications.ru.md) (Russian). + +### Auto-update + +**`update_ssh_monitor.sh`** can run on a schedule to `git pull`, compare SHA256 with `/usr/local/bin/ssh-monitor`, copy on change, and restart **`ssh-monitor.service`**. Does not modify `/etc/ssh-monitor.conf`. + +Setup guide: [docs/auto-update.ru.md](docs/auto-update.ru.md) (Russian). + ## Run modes Normal run (root): @@ -137,7 +153,7 @@ bash -n ./ssh-monitor ## Release tarball -Version is set in the script as **`SSH_MONITOR_VERSION`** (as of last doc update — **1.1.2-ipset**; see the **`ssh-monitor`** file for the current value). Build from the git tree: +Version is set in the script as **`SSH_MONITOR_VERSION`** (as of last doc update — **1.1.3-server-label**; see the **`ssh-monitor`** file for the current value). Build from the git tree: ```bash make dist diff --git a/ssh-monitor b/ssh-monitor index 8372d3e..ea2f2c0 100644 --- a/ssh-monitor +++ b/ssh-monitor @@ -7,7 +7,7 @@ IFS=$'\n\t' # ============================================ CONFIG_FILE="/etc/ssh-monitor.conf" -SSH_MONITOR_VERSION="1.1.2-ipset" +SSH_MONITOR_VERSION="1.1.3-server-label" TELEGRAM_BOT_TOKEN="" TELEGRAM_CHAT_ID="" @@ -38,6 +38,8 @@ DAILY_REPORT_TOP_IPS=5 DAILY_REPORT_TZ="" # IANA-зона для строк «🕐 Время» в Telegram/email. Пусто = тогда DAILY_REPORT_TZ, если она задана, иначе зона процесса (часто UTC у systemd — см. README). NOTIFY_TZ="" +# Подпись сервера в уведомлениях: пусто = hostname (+ IPv4); иначе фиксированное имя (удобно при одинаковых hostname). +SERVER_DISPLAY_NAME="" # Не слать повтор «Accepted» для той же пары user+IP чаще, чем раз в N секунд (разные port в журнале — один вход). SSH_ACCEPT_NOTIFY_DEDUP_SEC=5 @@ -162,6 +164,7 @@ load_config() { local cfg_daily_report_top_ips="${DAILY_REPORT_TOP_IPS}" local cfg_daily_report_tz="${DAILY_REPORT_TZ}" local cfg_notify_tz="${NOTIFY_TZ:-}" + local cfg_server_display_name="${SERVER_DISPLAY_NAME:-}" local cfg_ssh_accept_notify_dedup_sec="${SSH_ACCEPT_NOTIFY_DEDUP_SEC:-5}" local cfg_last_ssh_check_file="${LAST_SSH_CHECK_FILE}" local cfg_last_sudo_check_file="${LAST_SUDO_CHECK_FILE}" @@ -231,6 +234,7 @@ load_config() { DAILY_REPORT_TOP_IPS="${DAILY_REPORT_TOP_IPS:-$cfg_daily_report_top_ips}" DAILY_REPORT_TZ="$(trim_value "${DAILY_REPORT_TZ:-$cfg_daily_report_tz}")" NOTIFY_TZ="$(trim_value "${NOTIFY_TZ:-$cfg_notify_tz}")" + SERVER_DISPLAY_NAME="$(trim_value "${SERVER_DISPLAY_NAME:-$cfg_server_display_name}")" SSH_ACCEPT_NOTIFY_DEDUP_SEC="$(trim_value "${SSH_ACCEPT_NOTIFY_DEDUP_SEC:-$cfg_ssh_accept_notify_dedup_sec}")" if ! [[ "${SSH_ACCEPT_NOTIFY_DEDUP_SEC:-5}" =~ ^[0-9]+$ ]]; then SSH_ACCEPT_NOTIFY_DEDUP_SEC=5 @@ -477,6 +481,53 @@ write_log() { echo "${timestamp} - ${message}" } +# ============================================ +# Подпись сервера в текстах уведомлений +# ============================================ + +# Для Telegram и отчётов: «hostname (IPv4)» или SERVER_DISPLAY_NAME (+ IPv4); без IPv4 — только имя. +server_display_name_with_ip() { + local hn ip + if [ -n "${SERVER_DISPLAY_NAME:-}" ]; then + hn="${SERVER_DISPLAY_NAME}" + else + hn=$(hostname 2>/dev/null | tr -d '\r\n') + [ -z "$hn" ] && hn="(unknown)" + fi + ip="" + if command -v ip >/dev/null 2>&1; then + ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}') + [ -z "$ip" ] && ip=$(ip -4 route get 8.8.8.8 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}') + fi + if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then + if hostname -I >/dev/null 2>&1; then + ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | grep -Ev '^(127\.|0\.0\.0\.0)$' | head -n1) + fi + fi + if [ -n "$ip" ]; then + printf '%s (%s)' "$hn" "$ip" + else + printf '%s' "$hn" + fi +} + +# Вставляет «🖥️ Сервер: …» сразу после первой строки (заголовка), если строки ещё нет. +message_ensure_server_line() { + local msg="$1" + if [[ "$msg" == *'🖥️ Сервер:'* ]]; then + printf '%s' "$msg" + return 0 + fi + local first rest + first="${msg%%$'\n'*}" + if [ "$first" = "$msg" ]; then + printf '%s\n🖥️ Сервер: %s' "$first" "$(server_display_name_with_ip)" + else + rest="${msg#*$'\n'}" + printf '%s\n🖥️ Сервер: %s\n%s' "$first" "$(server_display_name_with_ip)" "$rest" + fi +} + # ============================================ # УВЕДОМЛЕНИЯ: telegram / email — по NOTIFY_CHAIN во все перечисленные каналы; BACKUP_WEBHOOK только если все неуспешны. # ============================================ @@ -603,6 +654,7 @@ PY notify_send() { local message="$1" + message="$(message_ensure_server_line "$message")" if [ "${DRY_RUN:-0}" = "1" ]; then printf '[DRY-RUN notify] %s\n' "$(printf '%s' "$message" | tr '\n' ' ')" >&2 return 0 @@ -1501,28 +1553,6 @@ monitor_logind() { echo "$current_time" >"$LAST_LOGIND_CHECK_FILE" } -# Для Telegram и отчётов: «hostname (IPv4)»; без доступного IPv4 — только hostname. -server_display_name_with_ip() { - local hn ip - hn=$(hostname 2>/dev/null | tr -d '\r\n') - [ -z "$hn" ] && hn="(unknown)" - ip="" - if command -v ip >/dev/null 2>&1; then - ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}') - [ -z "$ip" ] && ip=$(ip -4 route get 8.8.8.8 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}') - fi - if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then - if hostname -I >/dev/null 2>&1; then - ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | grep -Ev '^(127\.|0\.0\.0\.0)$' | head -n1) - fi - fi - if [ -n "$ip" ]; then - printf '%s (%s)' "$hn" "$ip" - else - printf '%s' "$hn" - fi -} - # ============================================ # Ежедневный отчёт # ============================================ @@ -1606,7 +1636,6 @@ send_daily_report() { fi local message=" 📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА "$'\n' - message="${message}🖥️ Сервер: $(server_display_name_with_ip)"$'\n' message="${message}🕐 Время отчета: ${current_time}"$'\n' message="${message}"$'\n' message="${message} 📈 СТАТИСТИКА ЗА ПОСЛЕДНИЕ 24 ЧАСА: "$'\n' @@ -1686,7 +1715,6 @@ send_heartbeat() { timestamp_human=$(notification_date '+%d.%m.%Y %H:%M:%S') local message=" ❤️ Heartbeat - скрипт мониторинга работает "$'\n' - message="${message}🖥️ Сервер: $(server_display_name_with_ip)"$'\n' message="${message}🕐 Время: ${timestamp_human}" notify_send "$message" @@ -1745,7 +1773,6 @@ send_startup_notification() { local message=" ✅ СКРИПТ МОНИТОРИНГА ЗАПУЩЕН "$'\n' message="${message}📌 Версия: ${SSH_MONITOR_VERSION:-?}"$'\n' - message="${message}🖥️ Сервер: $(server_display_name_with_ip)"$'\n' message="${message}🕐 Время запуска: ${timestamp}"$'\n' message="${message}📊 Мониторинг SSH, SUDO и systemd-logind активирован"$'\n' if [ "${ENABLE_IP_BAN:-1}" = "1" ]; then @@ -1768,7 +1795,6 @@ send_shutdown_notification() { local timestamp timestamp=$(notification_date '+%d.%m.%Y %H:%M:%S') local message=" ⚠️ СКРИПТ МОНИТОРИНГА ОСТАНОВЛЕН "$'\n' - message="${message}🖥️ Сервер: $(server_display_name_with_ip)"$'\n' message="${message}🕐 Время остановки: ${timestamp}" notify_send "$message" @@ -1826,6 +1852,7 @@ run_check_config() { printf 'DAILY_REPORT_HOUR=%s\n' "$DAILY_REPORT_HOUR" printf 'DAILY_REPORT_TZ=%s (пусто = зона процесса/date)\n' "${DAILY_REPORT_TZ:-}" printf 'NOTIFY_TZ=%s (пусто = DAILY_REPORT_TZ или зона процесса; см. README)\n' "${NOTIFY_TZ:-}" + printf 'SERVER_DISPLAY_NAME=%s (пусто = hostname; эффективно: %s)\n' "${SERVER_DISPLAY_NAME:-}" "$(server_display_name_with_ip)" printf 'SSH_ACCEPT_NOTIFY_DEDUP_SEC=%s\n' "${SSH_ACCEPT_NOTIFY_DEDUP_SEC:-5}" printf 'DAILY_REPORT_TOP_IPS=%s\n' "$DAILY_REPORT_TOP_IPS" printf 'BRUTE_WINDOW_SEC=%s BRUTE_MIN_FAILS=%s\n' "$BRUTE_WINDOW_SEC" "$BRUTE_MIN_FAILS" diff --git a/ssh-monitor.conf.example b/ssh-monitor.conf.example index afabc77..d227372 100644 --- a/ssh-monitor.conf.example +++ b/ssh-monitor.conf.example @@ -61,6 +61,8 @@ DAILY_REPORT_TZ="" DAILY_REPORT_TOP_IPS="5" # IANA-зона для «🕐 Время» в Telegram/email. Пусто = как DAILY_REPORT_TZ, иначе зона процесса (часто UTC в systemd — см. README). NOTIFY_TZ="" +# Подпись «🖥️ Сервер» в уведомлениях: пусто = hostname (+ IPv4); иначе фиксированное имя (например prod-web-01). +# SERVER_DISPLAY_NAME="prod-web-01" # Не чаще 1 успешного SSH (Accepted) в Telegram на пару user+IP за N секунд (разные port в журнале). 0 = только дедуп внутри одного прохода. SSH_ACCEPT_NOTIFY_DEDUP_SEC="5"