25 Commits

Author SHA1 Message Date
ssh-monitor 673145a199 Версия 1.0.0: релиз, Makefile (make dist), README и .gitignore для архива.
Made-with: Cursor
2026-04-14 13:52:26 +10:00
ssh-monitor 1a8352d65c Удалена интеграция Zabbix; остались Telegram и email (SMTP).
Made-with: Cursor
2026-04-14 12:20:04 +10:00
ssh-monitor d43afe03c6 Zabbix: передать ZABBIX_SERVER/HOST_NAME в окружение python3 для trapper
Made-with: Cursor
2026-04-14 11:47:25 +10:00
ssh-monitor ce6a3d4620 Zabbix: короткий plain для канала; seq в файле; alert через python3 -o
Made-with: Cursor
2026-04-14 11:41:03 +10:00
ssh-monitor 25f85d00fb Zabbix: усечение UTF-8 для trapper Character (ZABBIX_ALERT_MAX_BYTES)
Made-with: Cursor
2026-04-14 11:29:45 +10:00
ssh-monitor 6a4db2d7ad Zabbix alert: убрать NUL из текста перед trapper (-i)
Made-with: Cursor
2026-04-14 11:15:19 +10:00
ssh-monitor 6552b0225f Zabbix: успех только по failed:0 в ответе; seq через -k/-o; подсказка при отказе сервера
Made-with: Cursor
2026-04-14 11:10:30 +10:00
ssh-monitor 5e6b0dedb7 Zabbix: логировать stdout+stderr zabbix_sender; успех при failed: 0 в ответе
Made-with: Cursor
2026-04-14 11:06:11 +10:00
ssh-monitor 0e0333e7e4 Zabbix: два вызова zabbix_sender (alert и seq), лог stderr при ошибке
Made-with: Cursor
2026-04-14 10:57:32 +10:00
ssh-monitor eee84e5eb7 fix(notify): отправка во все каналы NOTIFY_ORDER, не только первый успех
- BACKUP_WEBHOOK только если все каналы неуспешны

- README/conf.example; версия 0.9.5.3

Made-with: Cursor
2026-04-14 10:50:38 +10:00
ssh-monitor 921fc9c795 fix: локальное время в уведомлениях (NOTIFY_TZ) и антидубль Accepted
- notification_date: NOTIFY_TZ → DAILY_REPORT_TZ → date процесса

- SSH_ACCEPT_NOTIFY_DEDUP_SEC: пауза между Telegram для той же пары user+IP

- версия 0.9.5.2, README и conf.example

Made-with: Cursor
2026-04-14 10:23:58 +10:00
ssh-monitor 57935e292e chore: версия 0.9.5.1
Made-with: Cursor
2026-04-14 10:14:22 +10:00
ssh-monitor 4975a6d842 fix: дубли SSH (journal + Accepted) и logind при Service=sshd
- journalctl: _COMM=sshd, fallback sshd/ssh; sort -u

- dedupe Accepted в одном проходе по user|ip|port

- LOGIND_SKIP_REMOTE: Type=ssh или Service=*sshd*, повторы loginctl

Made-with: Cursor
2026-04-14 10:12:40 +10:00
ssh-monitor 80d2508d52 feat: строка «Сервер» с основным IPv4 в старте/стопе, отчёте и heartbeat
Made-with: Cursor
2026-04-14 10:08:29 +10:00
ssh-monitor 415da821de fix: версия в Telegram при старте сразу под заголовком
Made-with: Cursor
2026-04-14 10:06:02 +10:00
ssh-monitor 02fa70722c docs: обязательный NOTIFY_CHAIN в README и ssh-monitor.conf.example
Made-with: Cursor
2026-04-14 09:56:18 +10:00
ssh-monitor da5d2638d7 fix: выход с ошибкой, если нет ни одного канала NOTIFY_CHAIN
Made-with: Cursor
2026-04-14 09:55:03 +10:00
ssh-monitor f52dda23ad docs: Zabbix и пояснение ZABBIX_SEQ_KEY в README
Made-with: Cursor
2026-04-14 09:52:07 +10:00
ssh-monitor 17737cbf09 feat(notify): каскад telegram → zabbix → email, NOTIFY_ORDER, SMTP, v0.9.5
Made-with: Cursor
2026-04-14 09:44:43 +10:00
PTah a1bfa6a003 chore: зафиксировать линию версий 0.9 (semver 0.9.0)
- Комментарий в скрипте и уточнение в README

Made-with: Cursor
2026-04-14 08:11:49 +10:00
PTah bdb10e82d8 feat: версия 0.9.0, --version и вывод в лог/health/старт
- SSH_MONITOR_VERSION + суффикс +git[-dirty] при запуске из репозитория

- README: раздел про версионирование

Made-with: Cursor
2026-04-14 08:05:50 +10:00
PTah 0f2b26b4b4 feat(heartbeat): строка сервера с основным IPv4 в скобках
Made-with: Cursor
2026-04-14 07:59:52 +10:00
PTah febe359248 fix(logind): не дублировать SSH при Type=tty и Service=sshd
- LOGIND_SKIP_REMOTE: учёт Service=sshd, короткие повторы loginctl при гонке

- README и пример конфига

Made-with: Cursor
2026-04-13 12:28:00 +10:00
PTah ced61c417a feat: единая инициализация меток LAST_* для SSH/sudo/logind
- checkpoint_stamp_init_if_bad: нет файла / битое значение → только метка «сейчас», без бэклога

- sudo/logind: убрано окно -30m при «первом» запуске (как у SSH)

- README: раздел про файлы-метки и отличие LAST_SECURITY_EVENTS_FILE

Made-with: Cursor
2026-04-13 10:06:57 +10:00
Луценко Андрей Анатольевич 3cfdc7b12c feat: мониторинг systemd-logind (journalctl), конфиг и антидубль SSH
- Опрос journalctl -u systemd-logind, LAST_LOGIND_CHECK_FILE, LOGIND_*

- README: параметры logind, ротация LAST_*, journalctl

Made-with: Cursor
2026-04-13 09:45:41 +10:00
26 changed files with 228 additions and 4721 deletions
-4
View File
@@ -1,4 +0,0 @@
# Агентские скрипты — LF в git и на Linux (manifest SHA256 = git blob).
ssh-monitor text eol=lf
*.sh text eol=lf
version.txt text eol=lf
+1 -11
View File
@@ -1,14 +1,4 @@
.DS_Store .DS_Store
CalculatorApp/ CalculatorApp/
.cursor/
# .env — не коммитить; ssh-monitor.conf с секретами допустим только в закрытом зеркале (не в публичном GitHub)
.env
.env.*
# Артефакты локальной сборки (релизные архивы грузятся в GitHub Releases, в git не коммитим) # Артефакты локальной сборки (релизные архивы грузятся в GitHub Releases, в git не коммитим)
ssh-monitor-*.tar.gz ssh-monitor-*.tar.gz
# Локальный загрузчик Rutube / yt-dlp — живёт в другом репозитории
rutube-download.py
.ytdl-archive.txt
.cursorignore
-24
View File
@@ -1,24 +0,0 @@
ssh-monitor (ветка ipset) — что сделать на сервере
==================================================
1) apt install ipset
2) Скопируйте ssh-monitor в /usr/local/bin/ (или как у вас принято), права +x.
3) Каталог /etc/iptables должен существовать, если нужен save правил:
mkdir -p /etc/iptables
Скрипт пишет:
- /etc/iptables/ipset.rules
- /etc/iptables/rules.v4 и rules.v6 (если есть iptables-save)
4) Загрузка ipset после перезагрузки (Debian/Ubuntu, пример):
apt install ipset-persistent
либо свой systemd unit: ipset restore -f /etc/iptables/ipset.rules
5) UFW / nftables: если фактический фильтр не iptables-nft совместимый с -m set,
правило может не примениться — проверяйте «iptables -L INPUT -n» и счётчики.
6) С таймаутом ipset ядро само убирает IP из набора; скрипт лишь чистит файл бан-листа
и при расхождении восстанавливает элемент в ipset (verify_ban_list_vs_ipset).
Версия скрипта: см. SSH_MONITOR_VERSION в начале файла.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 PTah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -4
View File
@@ -1,13 +1,10 @@
# Версия берётся из ssh-monitor (SSH_MONITOR_VERSION). # Версия берётся из ssh-monitor (SSH_MONITOR_VERSION).
.PHONY: dist clean-dist manifest .PHONY: dist clean-dist
VERSION := $(shell sed -n 's/^SSH_MONITOR_VERSION="\(.*\)".*/\1/p' ssh-monitor | head -n1) VERSION := $(shell sed -n 's/^SSH_MONITOR_VERSION="\(.*\)".*/\1/p' ssh-monitor | head -n1)
DIST := ssh-monitor-$(VERSION).tar.gz DIST := ssh-monitor-$(VERSION).tar.gz
dist: $(DIST) dist: $(DIST)
manifest:
./scripts/build-release-manifest.sh
$(DIST): ssh-monitor $(DIST): ssh-monitor
git archive --format=tar.gz --prefix=ssh-monitor-$(VERSION)/ -o $(DIST) HEAD git archive --format=tar.gz --prefix=ssh-monitor-$(VERSION)/ -o $(DIST) HEAD
+43 -70
View File
@@ -1,27 +1,9 @@
# ssh-monitor # ssh-monitor
**Версия:** `2.2.2-SAC` Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`systemd-logind`** (локальные/графические сессии и др.) с уведомлениями в **Telegram** и по **электронной почте (SMTP)**, опциональным резервным webhook, авто-блокировкой IP (IPv4 через `iptables`, IPv6 через `ip6tables` при наличии) и ежедневным отчётом.
Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, бан IP (ipset), ежедневный отчёт, heartbeat.
**Security Alert Center:** модуль **`sac-client.sh`**, режим `UseSAC` (`off` | `exclusive` | `dual` | `fallback`) — [контракт SAC](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). **Основной способ обновления на Linux** — кнопка **«Обновить ssh-monitor (SSH)»** в SAC: сервер передаёт `REPO_URL` / `GIT_BRANCH`, updater сам подтягивает git, выставляет права на конфиг/spool/state и перезапускает сервис. Cron/systemd timer — опционально.
**Безопасность (2.1.7+):** агент работает от root; доверяйте только своему git-зеркалу (`REPO_URL`) и `/etc/ssh-monitor.conf` (`chmod 600`). Updater дописывает обязательные ключи (`CONFIG_STRICT_PERMS`, `SAC_TLS_INSECURE`, SAC-*) и **на каждом прогоне** применяет `chmod`/`chown` к конфигу, SAC-spool и state-файлам. Подробнее: [docs/security-roadmap.ru.md](docs/security-roadmap.ru.md).
## Состав репозитория
| Файл | Назначение |
|------|------------|
| **`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 + manifest verify, `sac-client.sh`, перезапуск `ssh-monitor.service`. См. [docs/auto-update.ru.md](docs/auto-update.ru.md). |
**Системные пакеты и зависимости** (что поставить на сервер до запуска): пошагово в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md).
## Конфигурация ## Конфигурация
> **Закрытый инстанс Gitea:** в доверенном закрытом репозитории допустимо хранить **`ssh-monitor.conf`** с боевыми токенами (файл не в `.gitignore`). В публичном [GitHub](https://git.kalinamall.ru/PapaTramp/ssh-monitor) такой файл игнорируется — используйте только `ssh-monitor.conf.example`.
Скрипт читает параметры из `/etc/ssh-monitor.conf` в формате `KEY="value"`. Скрипт читает параметры из `/etc/ssh-monitor.conf` в формате `KEY="value"`.
1. Скопируйте пример: 1. Скопируйте пример:
@@ -39,14 +21,13 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` - `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`
- `NOTIFY_ORDER` — список каналов (`telegram`, `email` и сокращения **tg**, **mail**); при каждом оповещении скрипт **пытается отправить во все перечисленные** каналы по порядку (не «первый успешный — и стоп»). Пусто = в цепочку попадают только настроенные каналы (порядок по умолчанию см. в `ssh-monitor.conf.example`). Если итоговая цепочка пуста — скрипт не стартует (см. раздел **«Каналы оповещений и пустой NOTIFY_CHAIN»**). - `NOTIFY_ORDER` — список каналов (`telegram`, `email` и сокращения **tg**, **mail**); при каждом оповещении скрипт **пытается отправить во все перечисленные** каналы по порядку (не «первый успешный — и стоп»). Пусто = в цепочку попадают только настроенные каналы (порядок по умолчанию см. в `ssh-monitor.conf.example`). Если итоговая цепочка пуста — скрипт не стартует (см. раздел **«Каналы оповещений и пустой NOTIFY_CHAIN»**).
- `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — отправка почты через **python3** (см. таблицу каналов ниже) - `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — отправка почты через **python3** (см. таблицу каналов ниже)
- `BACKUP_WEBHOOK_URL` *(deprecated)* резервная доставка JSON, если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить; удаление в 2.3.x - `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` - `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» ниже - `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — см. раздел «Мониторинг systemd-logind» ниже
- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_ENABLED` (1/0 — отчёт с агента; 0 = только SAC), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SERVER_DISPLAY_NAME` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` - `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS`
- `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC` - `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC`
- `PROMETHEUS_TEXTFILE_DIR` — каталог для `ssh_monitor.prom` (совместимость с node_exporter textfile collector) - `PROMETHEUS_TEXTFILE_DIR` — каталог для `ssh_monitor.prom` (совместимость с node_exporter textfile collector)
- `HEALTHCHECK_STATUS_FILE` — путь к JSON-файлу с меткой последней итерации цикла - `HEALTHCHECK_STATUS_FILE` — путь к JSON-файлу с меткой последней итерации цикла
- `ENABLE_IP_BAN``1` (по умолчанию): мониторинг и автобан через ipset; `0`: только мониторинг и уведомления (без ipset/бана)
- `BAN_TIME`, `MAX_ATTEMPTS`, `BAN_CHECK_INTERVAL`, `MONITOR_INTERVAL` - `BAN_TIME`, `MAX_ATTEMPTS`, `BAN_CHECK_INTERVAL`, `MONITOR_INTERVAL`
- `WHITELIST_IPS`, `WHITELIST_SUBNETS` (CSV, например `ip1,ip2`) - `WHITELIST_IPS`, `WHITELIST_SUBNETS` (CSV, например `ip1,ip2`)
- `WATCHDOG_MAX_HEARTBEAT_AGE`, `WATCHDOG_LOG_FILE`, `WATCHDOG_SERVICE_NAME`, `WATCHDOG_NOTIFY_ON_RECOVERY` - `WATCHDOG_MAX_HEARTBEAT_AGE`, `WATCHDOG_LOG_FILE`, `WATCHDOG_SERVICE_NAME`, `WATCHDOG_NOTIFY_ON_RECOVERY`
@@ -59,7 +40,7 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
- `TELEGRAM_CHAT_ID` — ID чата/пользователя, куда отправляются уведомления. - `TELEGRAM_CHAT_ID` — ID чата/пользователя, куда отправляются уведомления.
- `NOTIFY_ORDER` — CSV имён каналов (`telegram`, `email` или `tg`, `mail`). Для **каждого** оповещения выполняется попытка доставки **во все** перечисленные каналы (при ошибке одного остальные всё равно пробуются; неудачи пишутся в **`LOG_FILE`**). Пустая строка: автоматически собирается цепочка только из реально настроенных каналов (по умолчанию порядок telegram → email; критерии «настроен» — **таблица** в разделе **«Каналы оповещений и пустой NOTIFY_CHAIN»**). Если после сборки цепочки **нет ни одного** канала, скрипт **сразу завершается** с ошибкой: **«Не настроен ни один канал отправки оповещений»** (в том числе режим `--check-config` и `--dry-run`). - `NOTIFY_ORDER` — CSV имён каналов (`telegram`, `email` или `tg`, `mail`). Для **каждого** оповещения выполняется попытка доставки **во все** перечисленные каналы (при ошибке одного остальные всё равно пробуются; неудачи пишутся в **`LOG_FILE`**). Пустая строка: автоматически собирается цепочка только из реально настроенных каналов (по умолчанию порядок telegram → email; критерии «настроен» — **таблица** в разделе **«Каналы оповещений и пустой NOTIFY_CHAIN»**). Если после сборки цепочки **нет ни одного** канала, скрипт **сразу завершается** с ошибкой: **«Не настроен ни один канал отправки оповещений»** (в том числе режим `--check-config` и `--dry-run`).
- `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — параметры SMTP для канала `email` (отправка через **python3**; см. таблицу «настроен»). - `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — параметры SMTP для канала `email` (отправка через **python3**; см. таблицу «настроен»).
- `BACKUP_WEBHOOK_URL`*(deprecated)* URL резервной отправки; только fallback; **не засчитывается** как основной канал - `BACKUP_WEBHOOK_URL`URL для резервной отправки (тело JSON `{"text":"..."}`); только fallback при сбое основных каналов, **не засчитывается** как «настроенный канал» (см. шаг 3).
- `LOG_FILE` — путь к основному лог-файлу скрипта. - `LOG_FILE` — путь к основному лог-файлу скрипта.
- `LAST_HEARTBEAT_FILE` — файл с timestamp последнего heartbeat-сообщения. - `LAST_HEARTBEAT_FILE` — файл с timestamp последнего heartbeat-сообщения.
- `LAST_REPORT_FILE` — дата последнего ежедневного отчёта в формате `YYYY-MM-DD` (старый формат Unix-времени при первом запуске будет автоматически интерпретирован). - `LAST_REPORT_FILE` — дата последнего ежедневного отчёта в формате `YYYY-MM-DD` (старый формат Unix-времени при первом запуске будет автоматически интерпретирован).
@@ -74,11 +55,8 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
- `LOGIND_NOTIFY_FAILED``1` отправлять Telegram по строкам logind, содержащим *failed* (широкий фильтр; при необходимости отключите). - `LOGIND_NOTIFY_FAILED``1` отправлять Telegram по строкам logind, содержащим *failed* (широкий фильтр; при необходимости отключите).
- `LOGIND_SKIP_REMOTE``1` (по умолчанию): для новой сессии logind, если доступен `loginctl`, не отправляется второе Telegram, если сессия уже учтена в **`monitor_ssh`**: при **`Type=ssh`** или если **`Service`** указывает на **sshd** (часто **`Type=tty` + `Service=sshd`**); при пустом ответе `loginctl` делаются короткие повторы. Поставьте `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_HOUR` — час (0..23), после наступления которого в **текущих календарных сутках** (в выбранной ниже зоне) отправляется не более одного ежедневного отчёта.
- `DAILY_REPORT_ENABLED``1` (по умолчанию) отправлять `report.daily.ssh` с агента; **`0`** — не слать (суточный отчёт формирует только SAC, см. `SAC_DAILY_REPORT_*`).
- `DAILY_REPORT_TZ` — необязательная **IANA**-зона (`Europe/Moscow`, `Asia/Yekaterinburg`, …). Если **пусто**, для отчёта используется та же зона, что и у команды `date` у процесса монитора (как правило, совпадает с `timedatectl` / `/etc/localtime` на сервере). Если сервис запускается с `TZ=UTC` в unit-файле, без `DAILY_REPORT_TZ` отчёт ориентируется на **UTC** — тогда задайте явную зону в конфиге. - `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`. - `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).
- `SERVER_IPV4` — опциональный override для SAC `host.ipv4`. Если пусто, IPv4 определяется автоматически.
- `SSH_ACCEPT_NOTIFY_DEDUP_SEC` — не чаще одного Telegram по **успешному SSH (`Accepted`)** на одну пару **пользователь + IP** за указанное число **секунд** (по умолчанию **5**). Снимает дубли, когда в journal две строки с **разными портами клиента** за один вход. **`0`** — отключить этот антидубль (останется только дедупликация внутри одного прохода `monitor_ssh` по `user|ip|port`). - `SSH_ACCEPT_NOTIFY_DEDUP_SEC` — не чаще одного Telegram по **успешному SSH (`Accepted`)** на одну пару **пользователь + IP** за указанное число **секунд** (по умолчанию **5**). Снимает дубли, когда в journal две строки с **разными портами клиента** за один вход. **`0`** — отключить этот антидубль (останется только дедупликация внутри одного прохода `monitor_ssh` по `user|ip|port`).
- `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа. - `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа.
- `BRUTE_WINDOW_SEC` — окно (секунды) для оценки «массового» брутфорса по `journalctl`. - `BRUTE_WINDOW_SEC` — окно (секунды) для оценки «массового» брутфорса по `journalctl`.
@@ -86,8 +64,7 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
- `BRUTE_NOTIFY_COOLDOWN_SEC` — пауза между повторными уведомлениями по одному и тому же IP. - `BRUTE_NOTIFY_COOLDOWN_SEC` — пауза между повторными уведомлениями по одному и тому же IP.
- `PROMETHEUS_TEXTFILE_DIR` — если задан существующий каталог, на каждой итерации пишется `ssh_monitor.prom` с метрикой `ssh_monitor_last_loop_unixtime`. - `PROMETHEUS_TEXTFILE_DIR` — если задан существующий каталог, на каждой итерации пишется `ssh_monitor.prom` с метрикой `ssh_monitor_last_loop_unixtime`.
- `HEALTHCHECK_STATUS_FILE` — если задан, на каждой итерации обновляется JSON `{ "ts", "hostname", "dry_run" }`. - `HEALTHCHECK_STATUS_FILE` — если задан, на каждой итерации обновляется JSON `{ "ts", "hostname", "dry_run" }`.
- `ENABLE_IP_BAN` — включить автобан IP (`1`) или работать только как монитор с уведомлениями (`0`; **ipset** не требуется). - `BAN_TIME` — длительность бана IP в секундах.
- `BAN_TIME` — длительность бана IP в секундах (используется при `ENABLE_IP_BAN=1`).
- `MAX_ATTEMPTS` — число неудачных SSH-попыток до автоматического бана. - `MAX_ATTEMPTS` — число неудачных SSH-попыток до автоматического бана.
- `BAN_CHECK_INTERVAL` — интервал проверки просроченных банов и аудита согласованности бан-листа с firewall (секунды). - `BAN_CHECK_INTERVAL` — интервал проверки просроченных банов и аудита согласованности бан-листа с firewall (секунды).
- `MONITOR_INTERVAL` — пауза между итерациями основного цикла мониторинга (секунды). - `MONITOR_INTERVAL` — пауза между итерациями основного цикла мониторинга (секунды).
@@ -119,24 +96,6 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
Команда **`ssh-monitor --check-config`** выводит актуальные значения, в том числе параметры logind. Команда **`ssh-monitor --check-config`** выводит актуальные значения, в том числе параметры logind.
Ограничения на хостах **без 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).
### Обновление агента
**Рекомендуемый путь — SAC:** в карточке Linux-хоста кнопка **«Обновить ssh-monitor (SSH)»**. Участие админа на сервере не требуется: SAC передаёт `REPO_URL`/`GIT_BRANCH`, updater выполняет `git pull`, синхронизирует `sac-client.sh`, дописывает ключи конфига, **выставляет права** на конфиг/spool/state и перезапускает `ssh-monitor.service`.
Скрипт **`update_ssh_monitor.sh`** также можно запускать вручную или по расписанию (cron/systemd timer) с тем же `REPO_URL` — поведение идентично.
Пошаговая настройка: [docs/auto-update.ru.md](docs/auto-update.ru.md).
## Режимы запуска ## Режимы запуска
Обычный запуск (root): Обычный запуск (root):
@@ -165,34 +124,22 @@ bash -n ./ssh-monitor
## Релизный архив ## Релизный архив
Версия задаётся в **`SSH_MONITOR_VERSION`** в файле **`ssh-monitor`** и дублируется в **`version.txt`** (та же строка, что в шапке README). Модуль **`sac-client.sh`** использует ту же версию для SAC `product_version`. Сборка tarball из текущего git-дерева: Версия задаётся в скрипте переменной **`SSH_MONITOR_VERSION`** (текущая стабильная — **1.0.0**). Сборка tarball из текущего git-дерева:
```bash ```bash
make dist make dist
``` ```
Появится файл `ssh-monitor-<версия>.tar.gz` (через `git archive`). Готовые архивы для установки без клона репозитория прикладываются к [релизам на GitHub](https://git.kalinamall.ru/PapaTramp/ssh-monitor/releases). Появится файл `ssh-monitor-<версия>.tar.gz` (через `git archive`). Готовые архивы для установки без клона репозитория прикладываются к [релизам на GitHub](https://github.com/PTah/ssh-monitor/releases).
## Установка скрипта, конфигурации и systemd ## Автозапуск через systemd
### Автоматическая установка (рекомендуется)
```bash
sudo ./first_deploy.sh
# или: sudo ./update_ssh_monitor.sh --deploy
```
См. [docs/auto-update.ru.md](docs/auto-update.ru.md) — клон из git, копирование в `/usr/local/bin/`, `sac-client.sh`, пример `/etc/ssh-monitor.conf`, unit systemd.
### Ручная установка
1. Скопируйте скрипт в постоянное место: 1. Скопируйте скрипт в постоянное место:
- `sudo install -m 750 ./ssh-monitor /usr/local/bin/ssh-monitor` - `sudo install -m 750 ./ssh-monitor /usr/local/bin/ssh-monitor`
- `sudo install -m 644 ./sac-client.sh /usr/local/bin/sac-client.sh` 2. Убедитесь, что конфиг есть:
2. Убедитесь, что конфиг создан (см. раздел **«Конфигурация»** выше): - `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf` (если еще не создан)
- `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf`
- `sudo chmod 600 /etc/ssh-monitor.conf` - `sudo chmod 600 /etc/ssh-monitor.conf`
3. Создайте unit-файл `/etc/systemd/system/ssh-monitor.service` (или `sudo cp ssh-monitor.service.example ...`): 3. Создайте unit-файл `/etc/systemd/system/ssh-monitor.service`:
```ini ```ini
[Unit] [Unit]
@@ -230,14 +177,40 @@ sudo journalctl -u ssh-monitor -f
Пример для `logrotate` лежит в репозитории: `contrib/logrotate.d/ssh-monitor`. Скопируйте файл в `/etc/logrotate.d/` и при необходимости поправьте пути под ваши `LOG_FILE` / `WATCHDOG_LOG_FILE` и другие файлы состояния в `/var/log/` (в т.ч. `LAST_*`, если решите их ротировать отдельно). Пример для `logrotate` лежит в репозитории: `contrib/logrotate.d/ssh-monitor`. Скопируйте файл в `/etc/logrotate.d/` и при необходимости поправьте пути под ваши `LOG_FILE` / `WATCHDOG_LOG_FILE` и другие файлы состояния в `/var/log/` (в т.ч. `LAST_*`, если решите их ротировать отдельно).
## Статистика без systemd (`journalctl`)
Для корректного подсчёта событий за последние 24 часа по файлам `/var/log/auth.log*` рекомендуется наличие **Python 3** на сервере. При его отсутствии счётчики в ежедневном отчёте для «классического» syslog могут быть занижены (используется безопасный fallback).
**Мониторинг `systemd-logind`** и другие части, использующие **`journalctl`**, на таких системах **не выполняются** или дают неполные данные — ориентируйтесь на окружение с **systemd** и доступным журналом.
## Watchdog (автоматическое восстановление) ## Watchdog (автоматическое восстановление)
Watchdog проверяет активность `ssh-monitor.service` и актуальность heartbeat (`LAST_HEARTBEAT_FILE`). При сбое выполняет `systemctl restart` и отправляет уведомление (Telegram и при сбое — `BACKUP_WEBHOOK_URL`, если задан). Watchdog проверяет:
Пошаговая установка скрипта **ssh-monitor-watchdog** и **timer** в **systemd**: раздел 6 в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md). - активен ли `ssh-monitor.service`;
- не устарел ли heartbeat (`LAST_HEARTBEAT_FILE`).
**Ключевые темы (для поиска):** мониторинг **SSH** и **sshd**, **bash**-скрипт для **Linux**-сервера, уведомления в **Telegram** и по **SMTP** / электронной почте, **systemd-logind**, **sudo**, **journalctl**, **iptables** / **ip6tables**, автоматический **бан IP** и **whitelist**, **systemd** unit, **ежедневный отчёт**, **heartbeat**, **OpenSSH**, безопасность сервера, **Prometheus** textfile, опционально **watchdog** для сервиса. Если сервис неактивен или heartbeat старше `WATCHDOG_MAX_HEARTBEAT_AGE`, watchdog выполняет `systemctl restart` и отправляет уведомление (Telegram и при сбое — `BACKUP_WEBHOOK_URL`, если задан).
## Лицензия ### Установка watchdog
Проект распространяется по лицензии **MIT** — см. файл [LICENSE](LICENSE). 1. Установите watchdog-скрипт:
- `sudo install -m 750 ./ssh-monitor-watchdog /usr/local/bin/ssh-monitor-watchdog`
2. Создайте unit и timer из примеров:
- `sudo cp ./ssh-monitor-watchdog.service.example /etc/systemd/system/ssh-monitor-watchdog.service`
- `sudo cp ./ssh-monitor-watchdog.timer.example /etc/systemd/system/ssh-monitor-watchdog.timer`
3. Примените и запустите timer:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ssh-monitor-watchdog.timer
sudo systemctl start ssh-monitor-watchdog.timer
```
4. Проверка:
```bash
sudo systemctl status ssh-monitor-watchdog.timer
sudo systemctl list-timers | grep ssh-monitor-watchdog
sudo journalctl -u ssh-monitor-watchdog.service -f
```
-174
View File
@@ -1,174 +0,0 @@
# Автообновление ssh-monitor
Скрипт **`update_ssh_monitor.sh`** обновляет **`/usr/local/bin/ssh-monitor`** из git и перезапускает systemd-сервис.
## Основной путь: Security Alert Center
На продакшен-хостах обновление идёт **через SAC**, без cron:
1. Админ нажимает **«Обновить ssh-monitor (SSH)»** в карточке хоста.
2. SAC по SSH запускает `/opt/scripts/update_ssh_monitor.sh` с `REPO_URL` / `GIT_BRANCH` из **Настройки → Обновления агентов**.
3. Updater сам выполняет `git pull`, синхронизирует `sac-client.sh` и watchdog, дописывает отсутствующие ключи в **`/etc/ssh-monitor.conf`**, **выставляет `chmod`/`chown`** на конфиг, SAC-spool, счётчики и state-файлы, перезапускает **`ssh-monitor.service`**.
Участие админа на сервере **не требуется** (кроме первичной настройки `SAC_URL` / `SAC_API_KEY` в конфиге).
При **первой установке** (нет updater на хосте) SAC выполняет bootstrap с **`--deploy`**: clone, ipset при необходимости, systemd unit, полный hardening.
**Cron / systemd timer** — опциональная альтернатива с тем же скриптом и обязательным `REPO_URL` в окружении.
Watchdog и таймеры watchdog **не изменяются** при обычном update (кроме синхронизации скрипта watchdog из git).
**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом update в **существующий** конфиг **дописываются** отсутствующие SAC-переменные и ключи безопасности; **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**.
Установка монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md).
## Что делает скрипт
1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`).
2. При **`--deploy`** / bootstrap SAC проверяет **`ipset`** и при отсутствии пытается установить через пакетный менеджер. Обычное обновление **не** вызывает `apt-get`.
3. В **`UPDATE_DIR`** выполняет **`git clone`** или **`git fetch` + checkout / `git pull --ff-only`** (без слепого `reset --hard`).
4. Проверяет **`release/manifest-VERSION.json`** (SHA256 файлов + commit) **до** копирования в `/usr/local/bin`.
5. Ищет **`ssh-monitor`** в клоне (GitLab layout или плоский репозиторий).
6. Сравнивает **SHA256** с **`LOCAL_SCRIPT_PATH`** (`/usr/local/bin/ssh-monitor`).
7. **Всегда** синхронизирует **`sac-client.sh`**, **`ssh-monitor-perms.sh`**, watchdog.
8. Дописывает ключи конфига; **`apply_runtime_security_hardening`**: `chmod 600` конфиг, `700` spool, `600` state/SAC-файлы, whitelist.
9. При изменении **`ssh-monitor`**, **`sac-client.sh`** или конфига — **`systemctl restart ssh-monitor.service`**.
10. Лог: **`/var/log/update_script.log`** (блок «Итог обновления», строки `manifest` и `security hardening`).
```mermaid
flowchart LR
A[SAC или cron] --> B[update_ssh_monitor.sh]
B --> C[git fetch/checkout]
C --> M{manifest OK?}
M -->|нет| X[exit 1]
M -->|да/skip| D[sync sac-client + perms lib]
C --> E{SHA256 ssh-monitor?}
D --> F[harden config/spool/state]
E -->|да| G[backup + cp ssh-monitor]
E -->|нет| F
G --> F
F --> H{что-то изменилось?}
H -->|да| I[restart ssh-monitor.service]
H -->|нет| J[без перезапуска]
```
## Настройка перед первым запуском
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона |
| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron |
| `GIT_BRANCH` | `main` | Ветка для clone/pull, если **`GIT_REF`** не задан |
| `GIT_REF` | *(пусто)* | Тег (`2.2.0-SAC`), ветка или commit SHA для checkout |
| `GIT_VERIFY_MODE` | `off` | `off` \| `tag` \| `commit` — см. раздел ниже |
| `GIT_ALLOW_RESET` | `0` | `1` = разрешить `git reset --hard` при расхождении истории (риск) |
| `RELEASE_MANIFEST_STRICT` | `0` | `1` = exit 1, если manifest для версии не найден |
| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать монитор |
| `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений |
| `UPDATE_VIA_SAC` | *(опционально)* | SAC выставляет `1`; при отсутствии бинарника включается `--deploy` |
Требования: **root**, **git**, **python3** (manifest verify), доступ к репозиторию.
## Pinned ref и manifest (2.2.0+)
Updater читает **`GIT_REF`**, **`GIT_VERIFY_MODE`**, **`GIT_ALLOW_RESET`** из env или `/etc/ssh-monitor.conf`.
### Режимы `GIT_VERIFY_MODE`
| Режим | Поведение | Когда |
|-------|-----------|-------|
| **`off`** | После fetch принять ref как есть | Тест, legacy; дефолт в скрипте |
| **`tag`** | `GIT_REF` должен существовать как тег | **Рекомендуется для прода** |
| **`commit`** | `GIT_REF` = полный SHA commit | Жёсткий pin без тегов |
Если **`GIT_REF`** пуст, используется **`GIT_BRANCH`** (`git pull --ff-only`). При невозможности fast-forward updater **не** делает `reset --hard` (только с **`GIT_ALLOW_RESET=1`**).
### Release manifest
В репозитории: **`release/manifest-VERSION.json`** — SHA256 ключевых файлов и `git_commit`. Updater сверяет manifest **после** git sync и **до** копирования бинарников.
- Файл найден и hash не совпал → **exit 1**, ничего не перезаписывается.
- Файл не найден → WARN, update продолжается (или exit 1 при **`RELEASE_MANIFEST_STRICT=1`**).
- Копирование **`update_ssh_monitor.sh`** в `/opt/scripts/` и re-exec — только если manifest для версии был и прошёл verify.
Генерация при релизе (хеши **из git**, как на Linux после `checkout` — не с диска Windows):
```bash
git add --renormalize ssh-monitor *.sh # при смене .gitattributes
./scripts/build-release-manifest.sh 2.2.2-SAC
git add release/manifest-2.2.2-SAC.json && git commit -m "release: manifest 2.2.2-SAC"
git tag 2.2.2-SAC
```
### Примеры для прода
**SAC / env на хосте** (через настройки SAC или unit):
```bash
REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git
GIT_REF=2.2.0-SAC
GIT_VERIFY_MODE=tag
```
**systemd timer** (`/etc/systemd/system/ssh-monitor-update.service`):
```ini
[Service]
Type=oneshot
Environment=REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git
Environment=GIT_REF=2.2.0-SAC
Environment=GIT_VERIFY_MODE=tag
ExecStart=/opt/scripts/update_ssh_monitor.sh
```
При доверенном закрытом зеркале **manifest + `GIT_REF=tag`** — достаточный минимум; GPG-подписи тегов — backlog (см. [security-roadmap.ru.md](security-roadmap.ru.md)).
## Первичная установка (--deploy)
```bash
sudo REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git ./update_ssh_monitor.sh --deploy
# или first_deploy.sh с REPO_URL в окружении
```
Через SAC bootstrap выполняется автоматически с **`--deploy`**.
## Установка updater (если не через SAC bootstrap)
```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 REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git /opt/scripts/update_ssh_monitor.sh
sudo tail -30 /var/log/update_script.log
```
## Запуск по расписанию (опционально)
### Cron
```
0 4 * * * REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git /opt/scripts/update_ssh_monitor.sh
```
### systemd timer
См. предыдущие примеры `ssh-monitor-update.service` / `.timer` с `Environment=REPO_URL=...`.
## Откат
```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
```
## Ограничения
- Секреты (`SAC_API_KEY`, Telegram) SAC **не** прописывает — только в `/etc/ssh-monitor.conf` на хосте.
- При недоступном git **`git pull`** пишет предупреждение и продолжает с локальной копией.
- **`REPO_URL`** без дефолта в скрипте — только доверенное зеркало (kalinamall / своё Gitea).
-152
View File
@@ -1,152 +0,0 @@
# Системные зависимости и установка окружения
Этот документ описывает, **какой софт** на сервере нужен для работы **ssh-monitor**, и **как его поставить** типовыми пакетами. Саму установку скрипта, конфигов и **systemd** смотрите в корневом [README.md](../README.md).
## 1. Что должно быть на хосте
### Обязательно
| Назначение | Что используется | Примечание |
|------------|------------------|------------|
| Интерпретатор | **Bash** (версия с `declare -A`) | Обычно уже есть (`/bin/bash`). |
| Запуск брандмауэра | **iptables**; для IPv6 — **ip6tables** (если баните IPv6) | Скрипт добавляет правила `DROP` для заблокированных IP. Нужен **root** (скрипт не стартует без root, кроме `--dry-run` / проверок). |
| Telegram | **curl** | Запросы к `api.telegram.org`. |
| Почта (канал `email`) | **Python 3** | SMTP через стандартную библиотеку. |
| Резервный webhook | **Python 3** + **curl** | Сборка JSON и `POST`. |
Без **хотя бы одного** настроенного канала (`telegram` или `email`) скрипт завершится с ошибкой — это не зависимости ОС, а настройка в `/etc/ssh-monitor.conf`.
### Настоятельно рекомендуется (полный функционал)
| Назначение | Что используется |
|------------|------------------|
| Журнал SSH/sudo/брутфорс, logind | **systemd** + **`journalctl`** |
| Дедупликация уведомлений logind для SSH | **`loginctl`** (часть **systemd**) |
| Удобный просмотр логов сервиса | **`journalctl`** |
На минималистичных системах без **journald** часть функций не работает или упрощается; отчёт за сутки по SSH может опираться на **`/var/log/auth.log`** (см. ниже).
### Утилиты из «базовой» системы
Обычно уже установлены: `grep`, `sed`, `awk`, `sort`, `date`, `who`, `mktemp`, `sudo` (если запускаете не напрямую от root), `wc`, `hostname`, `ip` (для подписи сервера в уведомлениях). Для дедупликации sudo в журнале желательны **`sha256sum`** или **`cksum`**.
---
## 2. Пошаговая установка пакетов (Debian / Ubuntu)
Выполняйте от пользователя с `sudo`.
**Шаг 1.** Обновить индексы пакетов:
```bash
sudo apt update
```
**Шаг 2.** Минимум для Telegram и бана по firewall:
```bash
sudo apt install -y iptables curl
```
При необходимости IPv6 (если на сервере используется `ip6tables`):
```bash
sudo apt install -y ip6tables
```
(На многих образах **ip6tables** уже входит в метапакет с iptables.)
**Шаг 3.** Если включите доставку почты (`MAIL_*` в конфиге) или **BACKUP_WEBHOOK_URL** (через Python):
```bash
sudo apt install -y python3
```
**Шаг 4.** Типовой сервер с **systemd** уже содержит `journalctl` и `loginctl`. Если ставите минимальный контейнер без них — для полного мониторинга нужен стек с **systemd** и **journald** (или см. раздел про ограничения без journald).
**Шаг 5.** (Опционально) Сохранение правил **iptables** после перезагрузки — отдельно от ssh-monitor, например:
```bash
sudo apt install -y iptables-persistent netfilter-persistent
```
Скрипт при наличии каталога **`/etc/iptables`** может записывать туда `rules.v4` / `rules.v6` (см. код `save_iptables_rules`). Имеет смысл создать каталог и настроить автозагрузку правил по документации вашего дистрибутива.
---
## 3. Другие дистрибутивы
Принцип тот же: пакеты **bash**, **iptables** (+ **ip6tables**), **curl**, **python3**; для полного функционала — **systemd** с **journald**.
Примеры:
- **RHEL / Alma / Rocky:** `dnf install iptables curl python3 systemd` (имена метапакетов могут отличаться).
- **Alpine:** `apk add bash iptables ip6tables curl python3` — отдельно проверьте наличие **systemd** / **journalctl** (на Alpine часто нет; мониторинг по journal будет недоступен).
---
## 4. Системы без `journalctl` (классический syslog)
- События **SSH** могут читаться из **`/var/log/auth.log`** (если скрипт не находит `journalctl`).
- **Ежедневная статистика** по этим файлам для отчёта **точнее с Python 3** (разбор логов в скрипте). Без Python отчёт может использовать упрощённый fallback.
- Блоки, завязанные на **`journalctl`** (**sudo** по журналу, **systemd-logind**, часть **security/brute**), на таком хосте **не дадут полного результата**.
---
## 5. Конфликты с UFW / другими обёртками firewall
Скрипт вставляет правила **iptables** самостоятельно. Если используете **UFW**, **firewalld** или только **nftables** без совместимости с iptables-nft, возможны конфликты или неожиданный порядок правил. После внедрения проверьте:
```bash
sudo iptables -L INPUT -n -v --line-numbers
```
При необходимости согласуйте порядок с вашей схемой управления firewall.
---
## 6. Опционально: watchdog
Скрипт **`ssh-monitor-watchdog`** проверяет сервис `ssh-monitor.service` и актуальность heartbeat; при сбое выполняет `systemctl restart`.
**Установка:**
1. Скопируйте скрипт:
`sudo install -m 750 ./ssh-monitor-watchdog /usr/local/bin/ssh-monitor-watchdog`
2. Установите unit и timer:
`sudo cp ./ssh-monitor-watchdog.service.example /etc/systemd/system/ssh-monitor-watchdog.service`
`sudo cp ./ssh-monitor-watchdog.timer.example /etc/systemd/system/ssh-monitor-watchdog.timer`
3. Включите timer:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ssh-monitor-watchdog.timer
sudo systemctl start ssh-monitor-watchdog.timer
```
4. Проверка:
```bash
sudo systemctl status ssh-monitor-watchdog.timer
sudo systemctl list-timers | grep ssh-monitor-watchdog
sudo journalctl -u ssh-monitor-watchdog.service -f
```
Параметры watchdog задаются в **`/etc/ssh-monitor.conf`** (переменные `WATCHDOG_*` в примере конфига).
---
## 7. Краткий чек-лист перед первым запуском
- [ ] Установлены **iptables** (и при необходимости **ip6tables**).
- [ ] Для Telegram установлен **curl**; для почты / webhook — **python3**.
- [ ] Настроен **`/etc/ssh-monitor.conf`** (минимум один канал уведомлений).
- [ ] Понятен порядок правил firewall на хосте (UFW и т.д.).
- [ ] (По желанию) настроено сохранение правил после перезагрузки.
Далее: установка самого **`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`**.
-98
View File
@@ -1,98 +0,0 @@
# Уведомления 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`** *(deprecated, legacy)*: только если **все** каналы цепочки не доставили сообщение. В типовом деплое не используется; удаление в 2.3.x.
Режим **`--dry-run`**: уведомления не отправляются; в stderr печатается текст **уже с подписью сервера**.
-258
View File
@@ -1,258 +0,0 @@
# Security roadmap — ssh-monitor
Чек-лист правок по аудиту безопасности. **Код пока не трогаем** — документ для совместного просмотра и утверждения фаз.
Связанные файлы: `ssh-monitor`, `update_ssh_monitor.sh`, `sac-client.sh`, `ssh-monitor-watchdog`, `ssh-monitor.conf.example`.
---
## Модель доверия (согласовано)
| Объект | Кто отвечает |
|--------|----------------|
| **`REPO_URL`** | Оператор. Указывает **своё** зеркало (kalinamall, GitHub после санитайза, форк). Жёсткий allowlist в коде **не нужен**. |
| **Закрытое зеркало** | Доверенный git в LAN/VPN — основной путь обновления. |
| **Публичный GitHub** | Санитизированная копия без секретов; обновление оттуда допустимо, если оператор сам прописал URL. |
| **`/etc/ssh-monitor.conf`** | Доверенный root-only файл; агент работает от root. |
| **Аудиты** | Нерегулярно; roadmap закрывает разумный baseline, не «вечный SOC». |
---
## Порядок релизов
| Версия | Фазы | Суть |
|--------|------|------|
| **2.1.7-SAC** | Фаза 1 + выбранное из Фазы 4 | Права, `REPO_URL` обязателен, webhook, docs | ✅ реализовано |
| **2.1.8-SAC** | Дополнение к 1.1 | SAC-first update: hardening прав на каждом прогоне updater | ✅ реализовано |
| **2.1.9-SAC** | Дополнение к 1.x | Watchdog: `🖥️ Сервер:` в Telegram (как у агента) | ✅ реализовано |
| **2.2.1-SAC** | Дополнение к 2.2.0 | Подавление lifecycle/watchdog TG при SAC-update | ✅ реализовано |
| **2.2.0-SAC** | **Фаза 2** | Manifest, pinned ref, без слепого `reset --hard` | ✅ реализовано |
| **2.3.0-SAC** | Фаза 3 | Парсер конфига без `source` (minor breaking) |
Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом релизе.
**Текущий статус (2026-07-08):** фаза 2 закрыта (2.2.0-SAC). **Дальше — фаза 3 → релиз 2.3.0-SAC.**
Backlog по ingest при массовом update (SAC + ops): [ingest-mass-update-backlog.ru.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/ingest-mass-update-backlog.ru.md) (отдельный ToDo, не блокирует 2.2.0).
---
## Фаза 1 — 2.1.7-SAC
### 1.1 Права на state / spool (M4)
- [x] При создании каталогов и файлов состояния: `chown root:root`, каталоги `chmod 700`, файлы `chmod 600`
- [x] Затронуть: `SAC_SPOOL_DIR`, `SAC_FAIL_COUNT_FILE`, heartbeat / last_* (где создаёт `ssh-monitor` / `sac-client.sh` / deploy)
- [x] **2.1.8:** `update_ssh_monitor.sh``apply_runtime_security_hardening` на **каждом** update (SAC, cron, вручную): retrofit `chmod`/`chown` существующих путей из конфига
- [x] В `--check-config`: предупреждение, если существующие пути с ослабленными правами
**Файлы:** `ssh-monitor`, `sac-client.sh`, при необходимости `update_ssh_monitor.sh`
---
### 1.2 Проверка прав конфига при старте (C1, частично)
- [x] Перед загрузкой конфига: если `/etc/ssh-monitor.conf` существует — проверить `root:root`, mode `600` или `400`
- [x] **Поведение по умолчанию:** `WARN` в лог + stderr, работа продолжается (не ломать старые установки с `644`)
- [x] Опционально в конфиге: `CONFIG_STRICT_PERMS=1`**exit 1** при нарушении
**Решение агента:** strict выключен по умолчанию; в README рекомендовать `chmod 600` и позже `400`.
---
### 1.3 `BACKUP_WEBHOOK_URL` (H3) — **вариант A (soft-deprecate)**
- [x] В `ssh-monitor.conf.example`: закомментировать / убрать из «активного» блока, комментарий `DEPRECATED: не используется в типовом деплое; будет удалён в 2.3.x`
- [x] В README / `docs/notifications.ru.md`: пометить **legacy / необязательно**
- [x] В `--check-config`: если задан — `WARN: BACKUP_WEBHOOK_URL deprecated`
- [ ] **Удаление кода** — отложить до **2.3.0** или позже, если подтвердим, что нигде не нужен
---
### 1.4 `REPO_URL` — только из env, без дефолта в коде (C3) — **утверждено**
- [x] Убрать захардкоженный default `https://git.kalinamall.ru/...` из `update_ssh_monitor.sh`
- [x] При старте updater: если `REPO_URL` пуст — **exit 1**, сообщение в **stderr** и **`$LOG_FILE`**
- [x] Текст ошибки: что задать (`export REPO_URL=...` или в systemd unit `Environment=REPO_URL=...`)
- [x] SAC: основной путь обновления (кнопка в UI); cron/timer — опционально. Bootstrap SAC → `--deploy`
- [x] **Не** делать allowlist доменов — оператор сам выбирает зеркало
**Файлы:** `update_ssh_monitor.sh`, `docs/auto-update.ru.md`, пример unit/timer если есть
**Заметка:** после 2.1.7 на всех хостах нужно явно прописать `REPO_URL` до следующего обновления.
---
### 1.5 Документация threat model
- [x] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub
- [x] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно
---
## Фаза 2 — 2.2.0-SAC
### 2.1 Pinned ref и режим верификации (C2) — **утверждено, детали на агенте**
Новые переменные (env или ключи в `/etc/ssh-monitor.conf`, читаемые updater’ом):
| Переменная | Назначение | Рекомендация |
|------------|------------|--------------|
| **`GIT_REF`** | Что checkout: тег (`2.2.0-SAC`), ветка (`main`) или commit SHA | **Тег релиза** — оптимально для прода |
| **`GIT_VERIFY_MODE`** | `off` \| `tag` \| `commit` | См. ниже |
**`GIT_VERIFY_MODE`:**
| Значение | Поведение | Когда использовать |
|----------|-----------|-------------------|
| **`off`** | После fetch принять ref как есть (как сейчас, но без `reset --hard` по умолчанию) | Тест, если manifest ниже достаточен |
| **`tag`** | `GIT_REF` должен быть **аннотированным тегом**; опционально `git tag -v` (см. 2.4) | **Рекомендуется для прода** |
| **`commit`** | `GIT_REF` = полный SHA; сверка с manifest | Жёсткий pin без тегов |
- [x] Реализовать чтение `GIT_REF` / `GIT_VERIFY_MODE` в `update_ssh_monitor.sh`
- [x] Дефолты для 2.2.0: `GIT_REF` пуст → `GIT_BRANCH`; `GIT_VERIFY_MODE=off` в скрипте, **`tag`** в prod-документации
- [x] **`docs/auto-update.ru.md`**: отдельный раздел с примерами systemd и таблицей режимов
---
### 2.2 Без слепого `reset --hard` (C2) — **утверждено**
- [x] При failed `git pull --ff-only`: **не** делать `reset --hard` автоматически
- [x] Лог + stderr: «история разошлась, требуется ручное вмешательство или новый clone»
- [x] Опционально: `GIT_ALLOW_RESET=1` только для ручного/CI (документировать риск)
---
### 2.3 Release manifest (C2, C4) — **утверждено**
- [x] Файл в репо, например `release/manifest-2.2.0-SAC.json`:
```json
{
"version": "2.2.0-SAC",
"git_commit": "abc123...",
"files": {
"ssh-monitor": "sha256:...",
"sac-client.sh": "sha256:...",
"update_ssh_monitor.sh": "sha256:...",
"ssh-monitor-watchdog": "sha256:..."
}
}
```
- [x] Публиковать manifest при каждом релизе (в git, рядом с тегом)
- [x] Updater: после checkout сверять SHA256 файлов из клона с manifest **до** копирования в `/usr/local/bin`
- [x] Несовпадение → exit 1, ничего не перезаписывать
- [x] Скрипт/цель в `Makefile` или `scripts/build-release-manifest.sh` для генерации
**Связь с 2.4:** manifest из **того же** `REPO_URL` защищает от подмены файлов в clone и от «не того» коммита; не защищает от полной компрометации git-сервера (для этого GPG).
---
### 2.4 GPG-подписи тегов — **опционально, скорее отложить**
**Вопрос:** если обновляем только из своего `REPO_URL` (kalinamall), нужен ли GPG?
| Угроза | Manifest + pinned tag | + GPG tag |
|--------|----------------------|-----------|
| Подмена файлов в clone / wrong commit | Да | Да |
| Компрометация аккаунта git (вредоносный force-push) | **Нет** | Да (если ключ не украден) |
| Доверенный LAN git у оператора | Обычно достаточно manifest | Nice-to-have |
**Решение для roadmap:**
- [x] **2.2.0:** не блокировать релиз на GPG; `GIT_VERIFY_MODE=tag` без `-v`
- [ ] **Backlog:** `GIT_GPG_VERIFY=1` + ключ в `/etc/ssh-monitor/trusted-release-key.asc`, если позже понадобится
- [x] Документировать: при доверенном закрытом зеркале manifest + `GIT_REF=tag`**достаточный минимум**
---
### 2.5 Re-exec updater только после verify (C4) — **утверждено**
- [x] `UPDATER_REEXEC` / копирование `update_ssh_monitor.sh` в `/opt/scripts/` — только если manifest SHA256 совпал
- [x] Иначе: лог, старая версия updater остаётся, exit 1
---
## Фаза 3 — 2.3.0-SAC (парсер конфига)
### 3.1 Парсер без `source` (C1) — **утверждено**
- [ ] Whitelist ключей: `TELEGRAM_*`, `SAC_*`, `MAIL_*`, `NOTIFY_*`, числовые лимиты, пути и т.д.
- [ ] Формат строк: `KEY="value"` / `KEY='value'` / `KEY=value`
- [ ] Игнор `#` комментариев; неизвестные ключи — WARN (или сохранять в sidecar — не нужно)
**Файлы:** `ssh-monitor`, `ssh-monitor-watchdog` (если source конфиг)
---
### 3.2 Миграция — **решение агента**
- [ ] **2.3.0:** парсер по умолчанию; `source` **удалить**
- [ ] Синтаксис файла **не меняется** для пользователя — те же `KEY="value"`
- [ ] В release notes: «поведение то же, выполнение bash из конфига невозможно»
- [ ] `ssh-monitor --check-config` проверяет неизвестные/битые строки
- [ ] Отдельная команда миграции **не нужна** (формат тот же)
---
### 3.3 Валидация значений — **решение агента**
- [ ] URL: `SAC_URL`, `MAIL_SMTP_HOST` — схема https/http, не пустой host
- [ ] Enum: `UseSAC``off|exclusive|dual|fallback`
- [ ] Числа: существующие `validate_numeric_or_default` расширить
- [ ] IP/CIDR в `WHITELIST_*` — базовая проверка формата
- [ ] Ошибки валидации → exit 1 на старте и в `--check-config`
---
### 3.x Удаление `BACKUP_WEBHOOK` (если утвердили soft-deprecate в 1.3)
- [ ] Удалить код и документацию в **2.3.0** или **2.4.0**, если подтверждено неиспользование
---
## Фаза 4 — что делаем, что нет
| Пункт | Версия | Решение |
|-------|--------|---------|
| **4.4** `ensure_ipset_installed` не на каждый update | 2.1.7 | **Делаем:** только `--deploy` / `first_deploy.sh`; обычный update не вызывает `apt-get` | ✅ |
| **4.3** Права whitelist-файла | 2.1.7 | **Делаем:** при загрузке `/etc/ssh_monitor_whitelist.txt` — WARN если не root:root | ✅ |
| **4.5** JSON healthcheck через `json.dumps` | 2.2.0 | **Делаем:** мелкий fix L3 | ✅ |
| **4.1** Секреты не через environ в Python | — | **Не делаем** (мало выигрыша при root) |
| **4.2** Telegram token в URL | — | **Не делаем** (ограничение Bot API) |
| **4.6** systemd hardening (non-root) | — | **Не делаем** (ipset/iptables требуют root) |
| **M6** Ротация логов | — | Уже есть `contrib/logrotate.d/` — только упоминание в README |
| **M7** `set_config_kv` sed | 2.2.0 | **Делаем:** экранирование `/` и `"` в `val` при merge конфига | ✅ |
---
## Вне scope (согласовано не раздувать)
- Жёсткий allowlist `REPO_URL` / зеркал — оператор задаёт свой URL
- Cert pinning TLS
- HashiCorp Vault / systemd-creds для секретов (backlog на годы)
- Регулярные внешние аудиты
---
## Чек-лист «перед стартом работы завтра»
- [x] **Релиз 2.1.72.1.9** — фаза 1 + SAC-first + watchdog server label
- [x] **Релиз 2.2.0** — scope: 2.12.3, 2.5, 4.5, 4.7(M7); **2.4 GPG отложить**
- [ ] **Релиз 2.3.0** — scope: 3.13.3, удаление webhook (если A)
- [ ] **Дефолт `GIT_REF`** в 2.2.0: тег из `version.txt` vs `main`
- [x] **Миграция хостов:** SAC-first update, `known_hosts`, `REPO_URL` через SAC
- [ ] **Ingest mass-update backlog** — см. security-alert-center `docs/ingest-mass-update-backlog.ru.md`
---
## Открытые вопросы на завтра
1. **1.3**`BACKUP_WEBHOOK_URL`: soft-deprecate (A), удалить сразу (B), оставить (C)?
2. **1.2** — достаточно WARN по умолчанию или сразу strict на новых установках?
3. **2.1** — дефолт `GIT_REF`: тег `version.txt` или ветка `main`?
4. Подтверждение: **ни на одном хосте** не используется Slack/Discord webhook через `BACKUP_WEBHOOK_URL`?
---
*Документ создан для ревью. После утверждения — работа по фазам в отдельных коммитах с bump версии.*
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# Первичная установка ssh-monitor — обёртка над update_ssh_monitor.sh --deploy
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/update_ssh_monitor.sh" --deploy "$@"
-220
View File
@@ -1,220 +0,0 @@
# ssh-monitor
Bash script that monitors **SSH**, **SUDO**, and **`systemd-logind`** events (local/graphical sessions, etc.), sends alerts via **Telegram** and **SMTP email**, optional backup webhook, automatic IP blocking (**ipset** + **iptables** / **ip6tables**: `hash:ip` sets with timeout, single `-m set` rule; details in **`IPSET-NOTES.txt`**), and a daily report.
## Repository contents
| File | Purpose |
|------|---------|
| **`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).
## Configuration
The script reads `/etc/ssh-monitor.conf` in `KEY="value"` format.
1. Copy the example:
- `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf`
2. Restrict permissions:
- `sudo chmod 600 /etc/ssh-monitor.conf`
3. Fill in at least **one notification channel** (otherwise the script exits immediately; see **«Mandatory requirement»** below and **«Notification channels and empty NOTIFY_CHAIN»**):
- for **Telegram**: `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`;
- or configure **SMTP** using the variables below with `NOTIFY_ORDER=""` (auto-selected channels).
**Mandatory requirement:** after loading the config, **`NOTIFY_CHAIN`** must contain **at least one** channel (`telegram` or `email`). If there are **none** (empty `NOTIFY_ORDER` and neither Telegram nor SMTP qualify as “configured”, or `NOTIFY_ORDER` contains only unknown names), the script **does not** enter the main loop and exits with code **1**, stderr: **`Не настроен ни один канал отправки оповещений`**. The same applies to **`--check-config`** and **`--dry-run`**. The backup **`BACKUP_WEBHOOK_URL`** is **not** part of this check — it is used only if **every** channel in **`NOTIFY_CHAIN`** fails to deliver.
Supported parameters:
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`
- `NOTIFY_ORDER` — channel list (`telegram`, `email`, or **tg**, **mail**); for each alert the script **tries all listed** channels in order (not “first success stops”). Empty = only configured channels are chained (default order in `ssh-monitor.conf.example`). If the resulting chain is empty — the script does not start (see **«Notification channels and empty NOTIFY_CHAIN»**).
- `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — mail via **python3** (see channel table below)
- `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), `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
- `ENABLE_IP_BAN``1` (default): monitoring and auto-ban via ipset; `0`: monitoring and alerts only (no ipset/ban)
- `BAN_TIME`, `MAX_ATTEMPTS`, `BAN_CHECK_INTERVAL`, `MONITOR_INTERVAL`
- `WHITELIST_IPS`, `WHITELIST_SUBNETS` (CSV, e.g. `ip1,ip2`)
- `WATCHDOG_MAX_HEARTBEAT_AGE`, `WATCHDOG_LOG_FILE`, `WATCHDOG_SERVICE_NAME`, `WATCHDOG_NOTIFY_ON_RECOVERY`
If `/etc/ssh-monitor.conf` is missing or some values are empty, script defaults apply.
### `/etc/ssh-monitor.conf` variables
- `TELEGRAM_BOT_TOKEN` — Telegram bot token for alerts.
- `TELEGRAM_CHAT_ID` — chat/user ID for notifications.
- `NOTIFY_ORDER` — CSV of channel names (`telegram`, `email`, `tg`, `mail`). For **each** alert, delivery is attempted **to all** listed channels (if one fails, others still run; failures go to **`LOG_FILE`**). Empty string: build chain from configured channels only (default order telegram → email; “configured” criteria — **table** in **«Notification channels and empty NOTIFY_CHAIN»**). If no channels remain, the script exits: **«Не настроен ни один канал отправки оповещений»** (including `--check-config` and `--dry-run`).
- `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — SMTP for `email` channel (**python3**; see “configured” table).
- `BACKUP_WEBHOOK_URL` — backup POST URL (JSON `{"text":"..."}`); fallback only, **not** counted as a configured channel (see step 3).
- `LOG_FILE` — main script log path.
- `LAST_HEARTBEAT_FILE` — file with last heartbeat timestamp.
- `LAST_REPORT_FILE` — date of last daily report `YYYY-MM-DD` (legacy Unix time on first run is interpreted automatically).
- `LAST_SSH_CHECK_FILE` — last SSH events check timestamp.
- `LAST_SUDO_CHECK_FILE` — last sudo events check timestamp.
- `LAST_SECURITY_EVENTS_FILE` — last “heavy” security journal check.
- `LAST_LOGIND_CHECK_FILE` — last `systemd-logind` journal processing (Unix time); ~30 minutes of history on first run.
- `BAN_LIST_FILE` — ban state file (IP, ban end time, metadata).
- `ENABLE_LOGIND_MONITOR``1` enables `journalctl -u systemd-logind`, `0` disables this block.
- `LOGIND_NOTIFY_NEW``1` send Telegram on new logind session (*New session … of user …*).
- `LOGIND_NOTIFY_REMOVED``1` send Telegram on session end (*Removed session …*); default `0` (log only to avoid spam).
- `LOGIND_NOTIFY_FAILED``1` send Telegram on logind lines containing *failed* (broad filter; disable if needed).
- `LOGIND_SKIP_REMOTE``1` (default): for a new logind session, if `loginctl` is available, skip duplicate Telegram when already covered by **`monitor_ssh`**: **`Type=ssh`** or **`Service`** points to **sshd** (often **`Type=tty` + `Service=sshd`**); short retries if `loginctl` returns empty. Set `0` for separate logind alerts for SSH sessions too.
- `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).
- `SERVER_IPV4` — optional override for SAC `host.ipv4`. If empty, the script auto-detects host IPv4.
- `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`.
- `BRUTE_MIN_FAILS` — minimum failures in the window to alert.
- `BRUTE_NOTIFY_COOLDOWN_SEC` — cooldown between repeat alerts for the same IP.
- `PROMETHEUS_TEXTFILE_DIR` — if set to an existing directory, writes `ssh_monitor.prom` with `ssh_monitor_last_loop_unixtime` each iteration.
- `HEALTHCHECK_STATUS_FILE` — if set, updates JSON `{ "ts", "hostname", "dry_run" }` each iteration.
- `ENABLE_IP_BAN` — enable auto-ban (`1`) or monitor-only with alerts (`0`; **ipset** not required).
- `BAN_TIME` — ban duration in seconds (`ENABLE_IP_BAN=1`).
- `MAX_ATTEMPTS` — failed SSH attempts before auto-ban.
- `BAN_CHECK_INTERVAL` — interval to check expired bans and reconcile ban list with firewall (seconds).
- `MONITOR_INTERVAL` — pause between main loop iterations (seconds).
- `WHITELIST_IPS` — comma-separated whitelist (IPv4/IPv6 exact match).
- `WHITELIST_SUBNETS`**IPv4-only** CIDR whitelist, comma-separated (e.g. `10.0.0.0/24`).
- `WATCHDOG_MAX_HEARTBEAT_AGE` — seconds after which heartbeat is stale.
- `WATCHDOG_LOG_FILE` — watchdog log file.
- `WATCHDOG_SERVICE_NAME` — systemd service watched by watchdog (default `ssh-monitor.service`).
- `WATCHDOG_NOTIFY_ON_RECOVERY``1` enables watchdog “OK” messages when healthy, `0` disables.
### Notification channels and empty NOTIFY_CHAIN
What counts as a **configured** channel (only these join the auto chain when **`NOTIFY_ORDER=""`**):
| Channel | “Configured” condition |
|---------|------------------------|
| `telegram` | **`TELEGRAM_BOT_TOKEN`** and **`TELEGRAM_CHAT_ID`** set |
| `email` | non-empty **`MAIL_SMTP_HOST`**, **`MAIL_FROM`**, **`MAIL_TO`**, **`python3`** in PATH |
If **`NOTIFY_CHAIN` is empty**, the script exits with **`Не настроен ни один канал отправки оповещений`** (see step 3 in **Configuration**).
### systemd-logind monitoring
- Collects **`systemd-logind`** unit messages via **`journalctl`** (`-o cat`): new/ended sessions, and lines containing *failed* (if enabled).
- Without **`journalctl`**, this block **does not run** (like other journal-based features).
- “New session” parsing expects typical English formats (`New session … of user …`). On mismatch, the line is still logged with a parse warning.
- **`loginctl`** is used for **`LOGIND_SKIP_REMOTE`**: **Type** and **Service** decide whether to avoid duplicate Telegram with **`monitor_ssh`** (see `LOGIND_SKIP_REMOTE` above).
- **`monitor_ssh`**: **sshd** events mostly **`journalctl _COMM=sshd`** (fallback units `sshd` / `ssh`), lines **`sort -u`**, **`Accepted`** dedup per **user|IP|port** within one pass; between passes **`SSH_ACCEPT_NOTIFY_DEDUP_SEC`** for **user|IP** (see config).
**`ssh-monitor --check-config`** prints current values, including logind settings.
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):
```bash
sudo bash ./ssh-monitor
```
Check config and syntax **without** starting the loop, **without** changing `iptables`, no mandatory Telegram send:
```bash
sudo bash ./ssh-monitor --check-config
```
**Dry-run** (firewall rules unchanged; notifications go to stderr instead of sending):
```bash
sudo bash ./ssh-monitor --dry-run
```
## Syntax check
```bash
bash -n ./ssh-monitor
```
## Release tarball
Version is set as **`SSH_MONITOR_VERSION`** in **`ssh-monitor`** and **`version.txt`** (same string as in the README header). **`sac-client.sh`** reports the same version to SAC. Build from the git tree:
```bash
make dist
```
Produces `ssh-monitor-<version>.tar.gz` (via `git archive`). Prebuilt archives are attached to [GitHub releases](https://git.kalinamall.ru/PapaTramp/ssh-monitor/releases).
## Installing the script, config, and systemd
1. Install the script:
- `sudo install -m 750 ./ssh-monitor /usr/local/bin/ssh-monitor`
2. Ensure config exists (see **Configuration**):
- `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf`
- `sudo chmod 600 /etc/ssh-monitor.conf`
3. Create `/etc/systemd/system/ssh-monitor.service`:
```ini
[Unit]
Description=SSH Monitor with Telegram alerts
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ssh-monitor
Restart=always
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
```
4. Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ssh-monitor
sudo systemctl start ssh-monitor
```
5. Status and logs:
```bash
sudo systemctl status ssh-monitor
sudo journalctl -u ssh-monitor -f
```
## Log rotation
Example `logrotate` config: `contrib/logrotate.d/ssh-monitor`. Copy to `/etc/logrotate.d/` and adjust paths for your `LOG_FILE` / `WATCHDOG_LOG_FILE` and state files under `/var/log/` (including `LAST_*` if you rotate them separately).
## Watchdog (automatic recovery)
The watchdog checks `ssh-monitor.service` activity and heartbeat freshness (`LAST_HEARTBEAT_FILE`). On failure it runs `systemctl restart` and sends a notification (Telegram, or `BACKUP_WEBHOOK_URL` if Telegram fails).
Step-by-step **ssh-monitor-watchdog** and **timer** setup: section 6 in [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md).
**Keywords (search):** SSH, **sshd**, **bash** on **Linux**, **Telegram**, **SMTP**, **systemd-logind**, **sudo**, **journalctl**, **iptables** / **ip6tables**, automatic **IP ban** and **whitelist**, **systemd** unit, **daily report**, **heartbeat**, **OpenSSH**, server security, **Prometheus** textfile, optional **watchdog**.
## License
This project is licensed under the **MIT License** — see [LICENSE](LICENSE).
-11
View File
@@ -1,11 +0,0 @@
{
"version": "2.2.0-SAC",
"git_commit": "275d305d627b462cae141605a002002525aa474c",
"files": {
"ssh-monitor": "sha256:517e4a210f271f57717b77746352ce2c60579cf89958c1c36501951901b885d0",
"sac-client.sh": "sha256:ffcbb0ed28e4247ccb5e74dd6e77d020f93eab8a17f9716e8913a17bc899274b",
"update_ssh_monitor.sh": "sha256:8108dd703095d54d51070933f074c90805f383967a3d2a6d63825898ad483231",
"ssh-monitor-watchdog": "sha256:5299bdcb481e7ca1d1042579d5c1ad8b13f3f72c1a6e922bcffd070b6c30ec80",
"ssh-monitor-perms.sh": "sha256:c67b56060141f67c651906b31afc220a1040c151fb00f60cc5f2511d6664dc11"
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"version": "2.2.1-SAC",
"git_commit": "",
"files": {
"ssh-monitor": "sha256:9fa2eaadceffe70358d83029668cc9bdc423697626f1c84a62552901490adcd9",
"sac-client.sh": "sha256:ffcbb0ed28e4247ccb5e74dd6e77d020f93eab8a17f9716e8913a17bc899274b",
"update_ssh_monitor.sh": "sha256:5e27e3514532500b84d8eca67bb968ee1dde365564ce4c3710422c43e090dc72",
"ssh-monitor-watchdog": "sha256:d1e786247433b6340a8fd2f1578abb2933149fc328f7d0dd7dcfd2319b94dedd",
"ssh-monitor-perms.sh": "sha256:86c919032294d47a4daf6681fd6e527babfacf778340dd4c0bcb33f63e5d063e"
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"version": "2.2.2-SAC",
"git_commit": "",
"files": {
"ssh-monitor": "sha256:be35da47fb1cf370628795bfdfe34195a7ba098c666bd8aa092929a168a10ef6",
"sac-client.sh": "sha256:4c481effd0e50b3db8cfa3baf3c5f228892530939df6f534a5d3d3d8a79db048",
"update_ssh_monitor.sh": "sha256:510d63b43ea02e5aaa4385bcc6c9396ad99637c377c9880125cb2168e9d422cd",
"ssh-monitor-watchdog": "sha256:41aa139cab0fb275249780953fceb3f8ebe01c49bf96f901dd3cf4d2fb3a9274",
"ssh-monitor-perms.sh": "sha256:53ebf15138d59be1610e5bc14a107b942d9a7fd039683abfd2fdd0aa0b54b933"
}
}
-520
View File
@@ -1,520 +0,0 @@
# SAC client for ssh-monitor (source from ssh-monitor)
# Версия для SAC = SSH_MONITOR_VERSION в ssh-monitor (отдельный номер здесь не задаётся).
# shellcheck shell=bash
sac_tls_insecure_enabled() {
case "${SAC_TLS_INSECURE:-0}" in
1 | yes | true | on) return 0 ;;
esac
return 1
}
sac_warn_tls_insecure() {
if sac_tls_insecure_enabled; then
write_log 'CRITICAL: SAC_TLS_INSECURE=1 — проверка TLS для SAC отключена (риск MITM). Только для краткой отладки в lab.'
fi
}
sac_curl() {
if sac_tls_insecure_enabled; then
curl -k -sS "$@"
else
curl -sS "$@"
fi
}
sac_normalize_mode() {
printf '%s' "${1:-off}" | tr '[:upper:]' '[:lower:]'
}
sac_is_configured() {
[ -n "${SAC_URL:-}" ] && [ -n "${SAC_API_KEY:-}" ]
}
sac_base_url() {
local url="${SAC_URL%/}"
case "$url" in
*/api/v1/events) url="${url%/api/v1/events}" ;;
esac
printf '%s' "${url%/}"
}
# URL ingest (POST): всегда .../api/v1/events, даже если в конфиге указан только хост
sac_ingest_url() {
local base
base="$(sac_base_url)"
[ -n "$base" ] || return 1
printf '%s/api/v1/events' "$base"
}
sac_agent_instance_id() {
local id_file="${SAC_AGENT_ID_FILE:-/var/lib/ssh-monitor/agent_instance_id}"
local dir
dir="$(dirname "$id_file")"
if [ ! -d "$dir" ]; then
ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || id_file="/tmp/ssh-monitor-agent_instance_id"
fi
if [ -f "$id_file" ]; then
cat "$id_file"
return 0
fi
local new_id
new_id="$(python3 -c 'import uuid; print(uuid.uuid4())')"
ssh_monitor_secure_file "$id_file" 2>/dev/null || true
printf '%s\n' "$new_id" >"$id_file" 2>/dev/null || true
printf '%s' "$new_id"
}
sac_check_health() {
local base code
base="$(sac_base_url)"
[ -n "$base" ] || return 1
if [ "${DRY_RUN:-0}" = "1" ]; then
return 0
fi
code="$(sac_curl -o /dev/null -w '%{http_code}' \
--connect-timeout "${SAC_TIMEOUT_SEC:-45}" \
--max-time "${SAC_TIMEOUT_SEC:-45}" \
"${base}/health" || echo 000)"
[ "$code" = "200" ]
}
sac_spool_write() {
local event_id="$1"
local body="$2"
local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || return 1
ssh_monitor_secure_file "${dir}/${event_id}.json" 2>/dev/null || true
printf '%s' "$body" >"${dir}/${event_id}.json"
}
sac_spool_remove() {
local event_id="$1"
local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
rm -f "${dir}/${event_id}.json"
}
sac_fail_count_file() {
printf '%s' "${SAC_FAIL_COUNT_FILE:-/var/lib/ssh-monitor/sac-fail.count}"
}
sac_read_fail_count() {
local f
f="$(sac_fail_count_file)"
if [ -f "$f" ]; then
cat "$f" 2>/dev/null | tr -dc '0-9'
else
printf '0'
fi
}
sac_write_fail_count() {
local n="$1"
local f
f="$(sac_fail_count_file)"
ssh_monitor_secure_file "$f" 2>/dev/null || mkdir -p "$(dirname "$f")" 2>/dev/null || true
printf '%s' "$n" >"$f"
}
sac_reset_fail_count() {
sac_write_fail_count 0
}
sac_increment_fail_count() {
local n max
max="${SAC_FALLBACK_FAILURES:-5}"
n="$(sac_read_fail_count)"
n=$((n + 1))
sac_write_fail_count "$n"
[ "$n" -ge "$max" ]
}
# В fallback: после N подряд ошибок POST не дергать SAC, пока /health снова не OK.
sac_should_attempt_send() {
local mode
mode="$(sac_normalize_mode "${UseSAC:-off}")"
[ "$mode" = "fallback" ] || return 0
local max n
max="${SAC_FALLBACK_FAILURES:-5}"
n="$(sac_read_fail_count)"
[ "$n" -lt "$max" ] && return 0
if sac_check_health; then
sac_reset_fail_count
write_log "SAC: /health восстановлен, снова отправляем в SAC (fallback)"
return 0
fi
write_log "WARN: SAC fallback: пропуск POST ($n>=$max сбоев подряд), только локальные каналы"
return 1
}
# POST готового JSON (ingest / повтор из spool). Возврат 0 при HTTP 201, 409 или 202 (legacy).
sac_post_payload() {
local payload="$1"
local event_id http_code tmp resp event_type
if ! sac_is_configured; then
return 1
fi
if [ "${DRY_RUN:-0}" = "1" ]; then
printf '[DRY-RUN SAC POST] %s\n' "$(printf '%s' "$payload" | head -c 120)" >&2
return 0
fi
if ! sac_should_attempt_send; then
return 1
fi
event_id="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin)["event_id"])')" || return 1
event_type="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("type","?"))')" || event_type="?"
tmp="$(mktemp)"
resp="$(mktemp)"
printf '%s' "$payload" >"$tmp"
http_code="$(sac_curl -o "$resp" -w '%{http_code}' \
-X POST "$(sac_ingest_url)" \
-H "Authorization: Bearer ${SAC_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${event_id}" \
--connect-timeout "${SAC_TIMEOUT_SEC:-45}" \
--max-time "${SAC_TIMEOUT_SEC:-45}" \
-d @"$tmp" 2>/dev/null || echo 000)"
rm -f "$tmp"
if [ "$http_code" = "201" ] || [ "$http_code" = "409" ] || [ "$http_code" = "202" ]; then
sac_spool_remove "$event_id" 2>/dev/null || true
sac_reset_fail_count
write_log "SAC: принято event_id=$event_id type=$event_type"
return 0
fi
write_log "WARN: SAC POST HTTP $http_code: $(tr '\n' ' ' <"$resp" | head -c 200)"
sac_spool_write "$event_id" "$payload"
sac_increment_fail_count || write_log "WARN: SAC fallback: достигнут порог SAC_FALLBACK_FAILURES"
return 1
}
# Список spool: report.daily.* первыми (старые раньше), затем остальное.
sac_flush_spool_list_files() {
local dir="${1:-}"
[ -n "$dir" ] && [ -d "$dir" ] || return 0
SPOOL_DIR="$dir" python3 <<'PY'
import glob
import json
import os
dir_path = os.environ.get("SPOOL_DIR", "")
daily, other = [], []
for path in glob.glob(os.path.join(dir_path, "*.json")):
event_type = ""
try:
with open(path, encoding="utf-8") as handle:
event_type = json.load(handle).get("type", "") or ""
except Exception:
pass
mtime = os.path.getmtime(path)
bucket = daily if event_type in ("report.daily.ssh", "report.daily.rdp") else other
bucket.append((mtime, path))
for bucket in (daily, other):
for _, path in sorted(bucket):
print(path)
PY
}
# Повторная отправка файлов из SAC_SPOOL_DIR (до max_files за вызов).
sac_flush_spool() {
local mode max_files="${1:-${SAC_SPOOL_FLUSH_MAX_FILES:-50}}"
mode="$(sac_normalize_mode "${UseSAC:-off}")"
case "$mode" in
off) return 0 ;;
esac
if ! sac_is_configured; then
return 0
fi
local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
[ -d "$dir" ] || return 0
local f count=0 payload
while IFS= read -r f; do
[ -n "$f" ] && [ -f "$f" ] || continue
count=$((count + 1))
[ "$count" -gt "$max_files" ] && break
payload="$(cat "$f")"
sac_post_payload "$payload" || true
done < <(sac_flush_spool_list_files "$dir")
return 0
}
# send_sac_event type severity title summary [details_json]
sac_send_event() {
local event_type="$1"
local severity="$2"
local title="$3"
local summary="$4"
local details_json="${5:-}"
if ! sac_is_configured; then
write_log "WARN: SAC не настроен (SAC_URL / SAC_API_KEY)"
return 1
fi
if [ "${DRY_RUN:-0}" = "1" ]; then
printf '[DRY-RUN SAC] %s | %s | %s\n' "$event_type" "$severity" "$title" >&2
return 0
fi
if ! sac_should_attempt_send; then
return 1
fi
local payload http_code event_id
payload="$(SAC_EVENT_TYPE="$event_type" \
SAC_SEVERITY="$severity" \
SAC_TITLE="$title" \
SAC_SUMMARY="$summary" \
SAC_DETAILS_JSON="$details_json" \
SAC_AGENT_ID="$(sac_agent_instance_id)" \
SAC_PRODUCT_VERSION="${SSH_MONITOR_VERSION:-unknown}" \
SAC_SERVER_DISPLAY_NAME="$(server_display_name_with_ip)" \
SAC_HOST_IPV4="${SERVER_IPV4:-}" \
python3 <<'PY'
import json, os, socket, uuid
from datetime import datetime, timezone
def details():
raw = os.environ.get("SAC_DETAILS_JSON", "").strip()
if not raw:
return None
return json.loads(raw)
def valid_ipv4(value: str) -> bool:
try:
parts = value.split(".")
return len(parts) == 4 and all(0 <= int(p) <= 255 for p in parts)
except Exception:
return False
def detect_host_ipv4() -> str:
manual = os.environ.get("SAC_HOST_IPV4", "").strip()
if manual and valid_ipv4(manual):
return manual
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("1.1.1.1", 53))
ip = s.getsockname()[0]
s.close()
if valid_ipv4(ip) and not ip.startswith(("127.", "169.254.")):
return ip
except Exception:
pass
try:
infos = socket.getaddrinfo(socket.gethostname(), None, family=socket.AF_INET)
for info in infos:
ip = info[4][0]
if valid_ipv4(ip) and not ip.startswith(("127.", "169.254.")):
return ip
except Exception:
pass
return ""
host = socket.gethostname()
display = os.environ.get("SAC_SERVER_DISPLAY_NAME", "").strip()
host_ipv4 = detect_host_ipv4()
etype = os.environ["SAC_EVENT_TYPE"]
if etype.startswith(("ssh.", "auth.", "rdp.")):
category = "auth"
elif etype.startswith("privilege."):
category = "privilege"
elif etype.startswith("session."):
category = "session"
elif etype.startswith("report."):
category = "report"
elif etype.startswith("rdg."):
category = "network"
else:
category = "agent"
payload = {
"schema_version": "1.0",
"event_id": str(uuid.uuid4()),
"occurred_at": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
"source": {
"product": "ssh-monitor",
"product_version": os.environ.get("SAC_PRODUCT_VERSION", "unknown"),
"agent_instance_id": os.environ["SAC_AGENT_ID"],
},
"host": {
"hostname": host,
"os_family": "linux",
**({"display_name": display} if display else {}),
**({"ipv4": host_ipv4} if host_ipv4 else {}),
},
"category": category,
"type": os.environ["SAC_EVENT_TYPE"],
"severity": os.environ["SAC_SEVERITY"],
"title": os.environ["SAC_TITLE"],
"summary": os.environ["SAC_SUMMARY"],
}
d = details()
if d is not None:
payload["details"] = d
print(json.dumps(payload, ensure_ascii=False))
PY
)" || return 1
sac_post_payload "$payload"
}
# JSON details из переменных SAC_D_* (опционально)
sac_build_details_json() {
python3 <<'PY'
import json, os
out = {}
for k, v in os.environ.items():
if k.startswith("SAC_D_") and v:
out[k[6:].lower()] = v
print(json.dumps(out) if out else "")
PY
}
sac_is_heartbeat_only_event() {
[ "${1:-}" = "agent.heartbeat" ]
}
sac_is_daily_report_event() {
case "${1:-}" in
report.daily.ssh|report.daily.rdp) return 0 ;;
*) return 1 ;;
esac
}
# Добавляет generated_by / telegram_via в JSON details для ingest.
sac_merge_notify_details() {
local details_json="${1:-}"
local telegram_via="${2:-}"
DETAILS_IN="$details_json" TG_VIA="$telegram_via" python3 <<'PY'
import json, os
d = {}
raw = os.environ.get("DETAILS_IN", "").strip()
if raw:
try:
d = json.loads(raw)
except Exception:
d = {}
d.setdefault("generated_by", "agent")
via = os.environ.get("TG_VIA", "").strip()
if via:
d.setdefault("telegram_via", via)
print(json.dumps(d, ensure_ascii=False))
PY
}
# notify_or_sac type severity title summary telegram_message [details_json]
notify_or_sac() {
local event_type="$1"
local severity="$2"
local title="$3"
local summary="$4"
local telegram_message="${5:-$summary}"
local details_json="${6:-}"
if [ -z "$details_json" ] && [ -n "${SAC_D_USER:-}${SAC_D_IP:-}${SAC_D_COMMAND:-}" ]; then
details_json="$(sac_build_details_json)"
fi
unset SAC_D_USER SAC_D_IP SAC_D_SOURCE_IP SAC_D_PORT SAC_D_COMMAND SAC_D_ATTEMPT SAC_D_MAX SAC_D_RUN_AS SAC_D_PWD SAC_D_SESSION_ID SAC_D_TTY SAC_D_PTS SAC_D_SINCE 2>/dev/null || true
local mode
mode="$(sac_normalize_mode "${UseSAC:-off}")"
local tg_via=""
case "$mode" in
exclusive|fallback) tg_via="sac" ;;
dual) tg_via="agent" ;;
esac
if [ -n "$tg_via" ]; then
details_json="$(sac_merge_notify_details "$details_json" "$tg_via")"
fi
# Периодический heartbeat — только SAC (браузер), без Telegram/email в любом режиме.
if sac_is_heartbeat_only_event "$event_type"; then
case "$mode" in
off)
return 0
;;
exclusive|dual|fallback)
sac_send_event "$event_type" "$severity" "$title" "$summary" "$details_json"
return $?
;;
*)
return 0
;;
esac
fi
# Суточный отчёт — только SAC/spool; при сбое ingest не дублировать в локальный Telegram.
if sac_is_daily_report_event "$event_type"; then
case "$mode" in
off)
return 0
;;
exclusive|dual|fallback)
if sac_send_event "$event_type" "$severity" "$title" "$summary" "$details_json"; then
return 0
fi
write_log "WARN: daily report не принят SAC — остаётся в spool (локальный Telegram пропущен)"
return 1
;;
*)
return 0
;;
esac
fi
case "$mode" in
off)
notify_send "$telegram_message"
;;
exclusive)
sac_send_event "$event_type" "$severity" "$title" "$summary" "$details_json"
;;
dual)
sac_send_event "$event_type" "$severity" "$title" "$summary" "$details_json" || true
NOTIFY_SKIP_SAC_MIRROR=1 notify_send "$telegram_message"
;;
fallback)
if sac_send_event "$event_type" "$severity" "$title" "$summary" "$details_json"; then
return 0
fi
NOTIFY_SKIP_SAC_MIRROR=1 notify_send "$telegram_message"
;;
*)
write_log "WARN: неизвестный UseSAC=$mode, только Telegram"
notify_send "$telegram_message"
;;
esac
}
run_check_sac() {
printf 'Проверка SAC (ssh-monitor)\n'
printf 'UseSAC=%s\n' "${UseSAC:-off}"
case "$(sac_normalize_mode "${UseSAC:-off}")" in
exclusive) printf 'Режим exclusive: уведомления только в SAC (Telegram/email с агента не отправляются)\n' ;;
dual) printf 'Режим dual: SAC + локальные каналы NOTIFY_CHAIN\n' ;;
fallback) printf 'Режим fallback: SAC, при сбое — NOTIFY_CHAIN\n' ;;
esac
printf 'SAC_URL=%s\n' "${SAC_URL:-}"
if sac_is_configured; then
printf 'SAC ingest URL=%s\n' "$(sac_ingest_url)"
fi
if ! sac_is_configured; then
printf 'SAC: SAC_URL или SAC_API_KEY не заданы\n' >&2
return 1
fi
if sac_check_health; then
printf 'SAC health: OK\n'
else
printf 'SAC health: FAIL\n' >&2
return 1
fi
if sac_send_event "agent.test" "info" "SAC test" "ssh-monitor --check-sac"; then
printf 'SAC ingest agent.test: OK (ожидается HTTP 201)\n'
return 0
fi
printf 'SAC ingest agent.test: FAIL\n' >&2
return 1
}
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
# Генерация release/manifest-VERSION.json (SHA256 файлов + git commit HEAD).
# Хеши считаются по содержимому git (HEAD), как после checkout на Linux-хосте.
# Запуск из корня репозитория: ./scripts/build-release-manifest.sh [VERSION]
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
version="${1:-}"
if [ -z "$version" ]; then
version="$(tr -d '\r\n' <version.txt 2>/dev/null || true)"
fi
if [ -z "$version" ]; then
version="$(grep -E '^[[:space:]]*SSH_MONITOR_VERSION=' ssh-monitor 2>/dev/null | head -1 | sed -E 's/^[^=]*=//; s/^["'\'']//; s/["'\'']$//; s/^[[:space:]]+//; s/[[:space:]]+$//')"
fi
[ -n "$version" ] || {
echo "ERROR: укажите VERSION или заполните version.txt" >&2
exit 1
}
FILES=(
ssh-monitor
sac-client.sh
update_ssh_monitor.sh
ssh-monitor-watchdog
ssh-monitor-perms.sh
)
for f in "${FILES[@]}"; do
git rev-parse "HEAD:$f" >/dev/null 2>&1 || [ -f "$f" ] || {
echo "ERROR: нет файла $f (ни в git HEAD, ни на диске)" >&2
exit 1
}
done
git_commit=""
if git rev-parse HEAD >/dev/null 2>&1; then
git_commit="$(git rev-parse HEAD)"
fi
out="release/manifest-${version}.json"
mkdir -p release
export MANIFEST_ROOT="$ROOT"
export MANIFEST_VERSION="$version"
export MANIFEST_GIT_COMMIT="$git_commit"
export MANIFEST_OUT="$out"
export MANIFEST_FILES="${FILES[*]}"
python3 <<'PY'
import hashlib
import json
import os
import subprocess
root = os.environ["MANIFEST_ROOT"]
version = os.environ["MANIFEST_VERSION"]
git_commit = os.environ.get("MANIFEST_GIT_COMMIT", "")
files = os.environ["MANIFEST_FILES"].split()
out = os.environ["MANIFEST_OUT"]
def sha256_bytes(data: bytes) -> str:
h = hashlib.sha256()
h.update(data)
return h.hexdigest()
def file_bytes_from_git(name: str) -> bytes | None:
for ref in (f":{name}", f"HEAD:{name}"):
try:
return subprocess.check_output(["git", "-C", root, "show", ref])
except subprocess.CalledProcessError:
continue
return None
def sha256_release_file(name: str) -> str:
blob = file_bytes_from_git(name)
if blob is not None:
return sha256_bytes(blob)
path = os.path.join(root, name)
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
manifest = {
"version": version,
"git_commit": git_commit,
"files": {},
}
for name in files:
manifest["files"][name] = f"sha256:{sha256_release_file(name)}"
with open(out, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
f.write("\n")
print(f"Wrote {out} commit={git_commit[:12] if git_commit else '?'}")
PY
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
# Проверка готовности хоста к пилоту UseSAC=exclusive (фаза 2.1).
set -euo pipefail
CONFIG="${CONFIG_FILE:-/etc/ssh-monitor.conf}"
MONITOR="${SSH_MONITOR_BIN:-/usr/local/bin/ssh-monitor}"
if [ -f "$CONFIG" ]; then
# shellcheck disable=SC1090
. "$CONFIG"
fi
fail=0
ok() { printf '[OK] %s\n' "$1"; }
bad() { printf '[FAIL] %s\n' "$1"; fail=1; }
mode="$(printf '%s' "${UseSAC:-off}" | tr '[:upper:]' '[:lower:]')"
[ "$mode" = "exclusive" ] || bad "UseSAC должен быть exclusive (сейчас: ${UseSAC:-off})"
[ -n "${SAC_URL:-}" ] && [ -n "${SAC_API_KEY:-}" ] || bad "Задайте SAC_URL и SAC_API_KEY"
[ -x "$MONITOR" ] || bad "Не найден $MONITOR"
[ -f "${MONITOR%/*}/sac-client.sh" ] || bad "Нет sac-client.sh рядом с ssh-monitor"
if "$MONITOR" --check-sac; then
ok "--check-sac"
else
bad "--check-sac"
fi
spool="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
pending=0
if [ -d "$spool" ]; then
pending=$(find "$spool" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l)
fi
printf ' Spool %s: %s файл(ов)\n' "$spool" "$pending"
if [ "$fail" -eq 0 ]; then
printf '\nПилот exclusive: хост готов. Сгенерируйте SSH/sudo событие и проверьте только в SAC UI.\n'
exit 0
fi
printf '\nИсправьте ошибки выше.\n'
exit 1
-156
View File
@@ -1,156 +0,0 @@
#!/usr/bin/env bash
# Публичный snapshot на github: без prod URL, токенов и kalinamall-only путей.
set -Eeuo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/ssh-monitor-github-snapshot.XXXX")"
trap 'rm -rf "$WORK"' EXIT
REF="${1:-HEAD}"
VERSION="$(git -C "$ROOT" show "$REF":version.txt 2>/dev/null | tr -d '[:space:]' || true)"
MSG="${2:-chore: public release ${VERSION:-snapshot} (SSH Monitor, no Cursor IDE traces)}"
echo "[snapshot] export $REF -> $WORK"
git -C "$ROOT" archive --format=tar "$REF" \
| tar -x -C "$WORK" \
--exclude='.cursor' \
--exclude='.env'
apply_sed() {
local expr=$1
if [[ "$(uname -s)" == Darwin ]]; then
find "$WORK" -type f \( -name '*.md' -o -name '*.sh' -o -name '*.example' \) \
! -path '*/scripts/push-github-snapshot.sh' \
-print0 | xargs -0 sed -i '' -E "$expr" 2>/dev/null || true
else
find "$WORK" -type f \( -name '*.md' -o -name '*.sh' -o -name '*.example' \) \
! -path '*/scripts/push-github-snapshot.sh' \
-print0 | xargs -0 sed -i -E "$expr" 2>/dev/null || true
fi
}
echo "[snapshot] rewrite hosts and internal URLs"
apply_sed 's#https://git\.kalinamall\.ru/PapaTramp#https://github.com/PTah#g'
apply_sed 's#git\.kalinamall\.ru/PapaTramp#github.com/PTah#g'
apply_sed 's#git\.kalinamall\.ru/papatramp#github.com/PTah#g'
apply_sed 's#git\.kalinamall\.ru#github.com#g'
apply_sed 's#sac\.kalinamall\.ru#sac.example.com#g'
apply_sed 's#remote add kalinamall#remote add origin#g'
apply_sed 's#remote set-url kalinamall#remote set-url origin#g'
apply_sed 's#local name=kalinamall#local name=origin#g'
apply_sed 's#всегда kalinamall#всегда origin#g'
cat >"$WORK/.gitignore" <<'EOF'
.DS_Store
CalculatorApp/
.env
.env.*
ssh-monitor.conf
ssh-monitor-*.tar.gz
rutube-download.py
.ytdl-archive.txt
EOF
cat >"$WORK/ssh-monitor.conf.example" <<'EOF'
# Example configuration — copy to /etc/ssh-monitor.conf and chmod 600.
# Never commit real tokens or passwords to git.
TELEGRAM_BOT_TOKEN="YOUR_TELEGRAM_BOT_TOKEN"
TELEGRAM_CHAT_ID="YOUR_TELEGRAM_CHAT_ID"
# At least one notification channel (telegram or email) is required.
NOTIFY_ORDER=""
# Security Alert Center (optional)
USE_SAC="off"
SAC_URL="https://sac.example.com/api/v1/events"
# SAC_API_KEY="your-ingest-api-key"
# SMTP (optional, requires python3)
MAIL_SMTP_HOST=""
MAIL_SMTP_PORT="587"
MAIL_SMTP_USER=""
MAIL_SMTP_PASSWORD=""
MAIL_FROM=""
MAIL_TO=""
MAIL_SMTP_STARTTLS="1"
MAIL_SMTP_SSL="0"
# Auto-update script default (override on your server)
# REPO_URL=https://github.com/PTah/ssh-monitor.git
EOF
if [[ -f "$WORK/README.md" ]]; then
python3 - "$WORK/README.md" <<'PY'
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
text = "".join(
line for line in text.splitlines(keepends=True)
if not line.startswith("> **Закрытый инстанс Gitea:**")
)
header = "# ssh-monitor\n\n**Версия:**"
if header in text and "Bash monitor for" not in text.split("**Версия:**", 1)[0]:
text = text.replace(
header,
"""# ssh-monitor
Bash monitor for **SSH**, **sudo**, and **systemd-logind** on Linux: Telegram/SMTP alerts, optional IP ban (ipset/iptables), daily report, SAC integration.
**Repository:** https://github.com/PTah/ssh-monitor
# ssh-monitor
**Версия:**""",
1,
)
old = "## Лицензия\n\nПроект распространяется по лицензии **MIT** — см. файл [LICENSE](LICENSE).\n"
new = """## Topics (GitHub)
`ssh`, `linux`, `telegram-bot`, `smtp`, `iptables`, `ipset`, `systemd`, `security`, `monitoring`, `openssh`
## License
MIT — see [LICENSE](LICENSE). Copyright (c) 2026 Andrey "PapaTramp" Lutsenko.
<details>
<summary><strong>English summary</strong></summary>
Single-file Bash agent parsing journalctl, notification chain, brute-force IP blocking, heartbeat, optional Prometheus textfile and Security Alert Center ingest via sac-client.sh.
</details>
"""
if old not in text:
raise SystemExit("README footer block not found; update push-github-snapshot.sh")
path.write_text(text.replace(old, new), encoding="utf-8")
PY
fi
if [[ -f "$WORK/LICENSE" ]]; then
if [[ "$(uname -s)" == Darwin ]]; then
sed -i '' 's/Copyright \(c\) 2026 PTah/Copyright (c) 2026 Andrey "PapaTramp" Lutsenko/' "$WORK/LICENSE"
else
sed -i 's/Copyright (c) 2026 PTah/Copyright (c) 2026 Andrey "PapaTramp" Lutsenko/' "$WORK/LICENSE"
fi
fi
echo "[snapshot] secret scan"
if grep -R -E 'AAE[a-zA-Z0-9_-]{30,}|sac_[A-Za-z0-9]{20,}|[0-9]{8,}:[A-Za-z0-9_-]{30,}' "$WORK" 2>/dev/null; then
echo " BLOCKED: possible secrets in snapshot tree"
exit 1
fi
echo "[snapshot] git commit in temp repo"
cd "$WORK"
git init -q
git checkout -q -b main
git add -A
git -c user.name="Andrey Lutsenko" -c user.email="papatramp@gmail.com" commit -q -m "$MSG"
echo "[snapshot] push github main (force snapshot)"
git remote add github "$(git -C "$ROOT" remote get-url github)"
git push github main --force
echo "[snapshot] OK -> github/main @ $(git rev-parse --short HEAD)"
+168 -710
View File
File diff suppressed because it is too large Load Diff
-207
View File
@@ -1,207 +0,0 @@
#!/bin/bash
# Path permission helpers for ssh-monitor, sac-client, watchdog, updater.
# shellcheck shell=bash
ssh_monitor_config_peek() {
local key="$1" file="$2" default="${3:-}"
local line val
[ -f "$file" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | tail -1)" || {
printf '%s\n' "$default"
return 0
}
val="${line#*=}"
val="${val#"${val%%[![:space:]]*}"}"
val="${val%"${val##*[![:space:]]}"}"
val="${val#\"}"
val="${val%\"}"
val="${val#\'}"
val="${val%\'}"
printf '%s\n' "$val"
}
ssh_monitor_truthy() {
case "${1,,}" in
1 | yes | true | on) return 0 ;;
esac
return 1
}
ssh_monitor_path_mode() {
local path="$1"
[ -e "$path" ] || return 1
stat -c '%a' "$path" 2>/dev/null || stat -f '%OLp' "$path" 2>/dev/null
}
ssh_monitor_path_owner() {
local path="$1"
[ -e "$path" ] || return 1
stat -c '%U:%G' "$path" 2>/dev/null
}
ssh_monitor_is_root_owned() {
local path="$1" own
[ -e "$path" ] || return 0
own="$(ssh_monitor_path_owner "$path" 2>/dev/null || true)"
[ "$own" = "root:root" ]
}
ssh_monitor_secure_dir() {
local dir="$1"
[ -n "$dir" ] || return 1
mkdir -p "$dir" 2>/dev/null || return 1
chmod 700 "$dir" 2>/dev/null || true
chown root:root "$dir" 2>/dev/null || true
}
ssh_monitor_secure_file() {
local file="$1"
local dir
[ -n "$file" ] || return 1
dir="$(dirname "$file")"
ssh_monitor_secure_dir "$dir" || true
if [ ! -f "$file" ]; then
: >"$file" 2>/dev/null || touch "$file" 2>/dev/null || return 1
fi
chmod 600 "$file" 2>/dev/null || true
chown root:root "$file" 2>/dev/null || true
}
# Существующий файл или каталог: 600/700 root:root; для отсутствующего пути — только родительский каталог.
ssh_monitor_harden_path() {
local path="$1"
[ -n "$path" ] || return 0
if [ -d "$path" ]; then
ssh_monitor_secure_dir "$path"
elif [ -f "$path" ] || [ -L "$path" ]; then
ssh_monitor_secure_file "$path"
else
local parent
parent="$(dirname "$path")"
[ -n "$parent" ] && [ "$parent" != "." ] && ssh_monitor_secure_dir "$parent"
fi
}
ssh_monitor_warn_path_perms() {
local path="$1" kind="$2" max_mode="${3:-600}"
local mode own msg
[ -e "$path" ] || return 0
if ! ssh_monitor_is_root_owned "$path"; then
own="$(ssh_monitor_path_owner "$path" 2>/dev/null || echo '?')"
msg="WARN: ${kind} ${path}: владелец ${own}, ожидается root:root"
printf '%s\n' "$msg" >&2
[ -n "${write_log:-}" ] && write_log "$msg" || true
fi
mode="$(ssh_monitor_path_mode "$path" 2>/dev/null || echo "")"
if [ -n "$mode" ] && [ "$((10#$mode))" -gt "$((10#$max_mode))" ]; then
msg="WARN: ${kind} ${path}: mode ${mode}, рекомендуется не шире ${max_mode}"
printf '%s\n' "$msg" >&2
[ -n "${write_log:-}" ] && write_log "$msg" || true
fi
}
ssh_monitor_check_config_file_perms() {
local file="${1:-/etc/ssh-monitor.conf}"
local strict="${2:-0}"
local mode msg
[ -f "$file" ] || return 0
if ! ssh_monitor_is_root_owned "$file"; then
msg="WARN: конфиг ${file}: владелец $(ssh_monitor_path_owner "$file" 2>/dev/null || echo '?'), ожидается root:root"
printf '%s\n' "$msg" >&2
[ -n "${write_log:-}" ] && write_log "$msg" || true
if ssh_monitor_truthy "$strict"; then
printf '%s\n' "CONFIG_STRICT_PERMS=1: исправьте владельца конфига (chown root:root ${file})" >&2
return 1
fi
fi
mode="$(ssh_monitor_path_mode "$file" 2>/dev/null || echo "")"
case "$mode" in
600 | 400) ;;
*)
msg="WARN: конфиг ${file}: mode ${mode:-?}, рекомендуется 600 или 400 (chmod 600 ${file})"
printf '%s\n' "$msg" >&2
[ -n "${write_log:-}" ] && write_log "$msg" || true
if ssh_monitor_truthy "$strict"; then
printf '%s\n' "CONFIG_STRICT_PERMS=1: ослабленные права конфига недопустимы" >&2
return 1
fi
;;
esac
return 0
}
# Подпись сервера в Telegram/webhook (как в ssh-monitor: SERVER_DISPLAY_NAME или hostname + IPv4).
ssh_monitor_server_display_name_with_ip() {
local hn ip
hn="${SERVER_DISPLAY_NAME:-}"
if [ -z "$hn" ]; then
hn="$(hostname 2>/dev/null | tr -d '\r\n')"
[ -z "$hn" ] && hn="(unknown)"
fi
ip="${SERVER_IPV4:-}"
if [ -z "$ip" ] && 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 && 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
if [ -n "$ip" ]; then
printf '%s (%s)' "$hn" "$ip"
else
printf '%s' "$hn"
fi
}
ssh_monitor_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" "$(ssh_monitor_server_display_name_with_ip)"
else
rest="${msg#*$'\n'}"
printf '%s\n🖥️ Сервер: %s\n%s' "$first" "$(ssh_monitor_server_display_name_with_ip)" "$rest"
fi
}
# Флаг штатного SAC/cron-update: подавляет lifecycle/watchdog Telegram на время прогона updater.
SSH_MONITOR_UPDATE_STATE_DEFAULT="/var/lib/ssh-monitor/agent-update-in-progress"
SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC="${SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC:-900}"
ssh_monitor_update_state_file() {
printf '%s\n' "${SSH_MONITOR_UPDATE_STATE_FILE:-$SSH_MONITOR_UPDATE_STATE_DEFAULT}"
}
ssh_monitor_update_in_progress() {
local f ts now age
f="$(ssh_monitor_update_state_file)"
[ -f "$f" ] || return 1
ts="$(tr -d '\r\n' <"$f" 2>/dev/null || true)"
[[ "$ts" =~ ^[0-9]+$ ]] || return 1
now=$(date '+%s')
age=$((now - ts))
[ "$age" -le "$SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC" ]
}
ssh_monitor_update_mark_begin() {
local f dir
f="$(ssh_monitor_update_state_file)"
dir="$(dirname "$f")"
ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || true
date '+%s' >"$f"
ssh_monitor_secure_file "$f" 2>/dev/null || chmod 600 "$f" 2>/dev/null || true
}
ssh_monitor_update_mark_end() {
local f
f="$(ssh_monitor_update_state_file)"
rm -f "$f" 2>/dev/null || true
}
+1 -18
View File
@@ -3,14 +3,9 @@ set -Eeuo pipefail
IFS=$'\n\t' IFS=$'\n\t'
CONFIG_FILE="/etc/ssh-monitor.conf" CONFIG_FILE="/etc/ssh-monitor.conf"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/ssh-monitor-perms.sh" ]; then
# shellcheck source=ssh-monitor-perms.sh
source "$SCRIPT_DIR/ssh-monitor-perms.sh"
fi
WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log" WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log"
LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt" LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt"
WATCHDOG_MAX_HEARTBEAT_AGE=18000 WATCHDOG_MAX_HEARTBEAT_AGE=5400
WATCHDOG_SERVICE_NAME="ssh-monitor.service" WATCHDOG_SERVICE_NAME="ssh-monitor.service"
WATCHDOG_NOTIFY_ON_RECOVERY="0" WATCHDOG_NOTIFY_ON_RECOVERY="0"
@@ -67,7 +62,6 @@ PY
notify_send() { notify_send() {
local message="$1" local message="$1"
message="$(ssh_monitor_message_ensure_server_line "$message")"
if send_telegram_raw "$message"; then if send_telegram_raw "$message"; then
return 0 return 0
fi fi
@@ -81,15 +75,9 @@ notify_send() {
load_config() { load_config() {
if [ -f "$CONFIG_FILE" ]; then if [ -f "$CONFIG_FILE" ]; then
local _strict_peek
_strict_peek="$(ssh_monitor_config_peek CONFIG_STRICT_PERMS "$CONFIG_FILE" "0")"
ssh_monitor_check_config_file_perms "$CONFIG_FILE" "$_strict_peek" || true
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$CONFIG_FILE" source "$CONFIG_FILE"
fi fi
if [ -n "${BACKUP_WEBHOOK_URL:-}" ]; then
write_log "WARN: BACKUP_WEBHOOK_URL deprecated (legacy fallback); будет удалён в 2.3.x"
fi
} }
is_service_active() { is_service_active() {
@@ -142,11 +130,6 @@ main() {
mkdir -p "$(dirname "$WATCHDOG_LOG_FILE")" mkdir -p "$(dirname "$WATCHDOG_LOG_FILE")"
touch "$WATCHDOG_LOG_FILE" touch "$WATCHDOG_LOG_FILE"
if ssh_monitor_update_in_progress 2>/dev/null; then
write_log "Watchdog: пропуск — идёт agent-update (state file)"
exit 0
fi
local reason="" local reason=""
if ! is_service_active; then if ! is_service_active; then
reason="systemd reported inactive service" reason="systemd reported inactive service"
+9 -49
View File
@@ -1,9 +1,11 @@
# Закрытое зеркало — доверенный инстанс; в публичном GitHub используйте example без секретов. TELEGRAM_BOT_TOKEN="replace_with_telegram_bot_token"
TELEGRAM_BOT_TOKEN="8239219522:AAEyOZX3cwNfgGOMDkf-mgjTIuoaOh5gF7I" TELEGRAM_CHAT_ID="replace_with_telegram_chat_id"
TELEGRAM_CHAT_ID="2843230"
# DEPRECATED: legacy fallback webhook (Slack/Discord). Не используется в типовом деплое; удаление в 2.3.x. # Резервный webhook, если ВСЕ каналы из NOTIFY_CHAIN не смогли доставить сообщение (JSON {"text":"..."}).
# BACKUP_WEBHOOK_URL="" # ВАЖНО: BACKUP_WEBHOOK_URL НЕ заменяет основной канал — без хотя бы одного из telegram/email
# скрипт сразу завершится с ошибкой в stderr: «Не настроен ни один канал отправки оповещений» (код 1),
# в том числе при --check-config и --dry-run. Сначала настройте Telegram или SMTP ниже.
BACKUP_WEBHOOK_URL=""
# --- Очередь оповещений (NOTIFY_ORDER) --- # --- Очередь оповещений (NOTIFY_ORDER) ---
# CSV: telegram, email (или сокращения tg, mail). # CSV: telegram, email (или сокращения tg, mail).
@@ -23,24 +25,6 @@ TELEGRAM_CHAT_ID="2843230"
# NOTIFY_ORDER="email" # NOTIFY_ORDER="email"
NOTIFY_ORDER="" NOTIFY_ORDER=""
# --- Security Alert Center (SAC) ---
# off | exclusive | dual | fallback — см. docs в security-alert-center (agent-integration.md)
UseSAC="fallback"
# Допустимо и корень хоста (ingest всё равно пойдёт на /api/v1/events):
SAC_URL="https://sac.kalinamall.ru"
SAC_API_KEY="sac_UkOsAT3UWiQS54KK5OJPBDCSucysQDrKFju28wmYiz8"
SAC_SPOOL_DIR="/var/lib/ssh-monitor/sac-spool"
SAC_FAIL_COUNT_FILE="/var/lib/ssh-monitor/sac-fail.count"
SAC_FALLBACK_FAILURES="5"
SAC_TIMEOUT_SEC="45"
SAC_SPOOL_FLUSH_MAX_FILES="50"
SAC_SPOOL_MAX_AGE_HOURS="72"
# 0 = проверять TLS SAC (рекомендуется). 1 = curl -k (только lab; CRITICAL в логе).
SAC_TLS_INSECURE="0"
# 0 = WARN при chmod конфига != 600. 1 = exit 1 при ослабленных правах /etc/ssh-monitor.conf.
CONFIG_STRICT_PERMS="0"
# SMTP (python3). Для авто-режима почты нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO. # SMTP (python3). Для авто-режима почты нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
MAIL_SMTP_HOST="" MAIL_SMTP_HOST=""
MAIL_SMTP_PORT="587" MAIL_SMTP_PORT="587"
@@ -53,8 +37,6 @@ MAIL_SMTP_SSL="0"
LOG_FILE="/var/log/ssh_monitor.log" LOG_FILE="/var/log/ssh_monitor.log"
LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt" LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt"
# Интервал SAC agent.heartbeat (минуты). 240 = 4 ч.
HEARTBEAT_INTERVAL_MINUTES="240"
LAST_REPORT_FILE="/var/log/last_daily_report.txt" LAST_REPORT_FILE="/var/log/last_daily_report.txt"
LAST_SSH_CHECK_FILE="/var/log/last_ssh_check" LAST_SSH_CHECK_FILE="/var/log/last_ssh_check"
LAST_SUDO_CHECK_FILE="/var/log/last_sudo_check" LAST_SUDO_CHECK_FILE="/var/log/last_sudo_check"
@@ -62,9 +44,6 @@ LAST_SECURITY_EVENTS_FILE="/var/log/last_security_events_check"
LAST_LOGIND_CHECK_FILE="/var/log/last_logind_check" LAST_LOGIND_CHECK_FILE="/var/log/last_logind_check"
BAN_LIST_FILE="/var/log/ssh_bans.txt" BAN_LIST_FILE="/var/log/ssh_bans.txt"
# 1 = мониторинг + автобан IP (нужен ipset). 0 = только мониторинг и уведомления (логины, неудачи, sudo, logind и т.д. — без ipset/iptables).
ENABLE_IP_BAN="1"
# systemd-logind: 1 = включено, 0 = выключено # systemd-logind: 1 = включено, 0 = выключено
ENABLE_LOGIND_MONITOR="1" ENABLE_LOGIND_MONITOR="1"
LOGIND_NOTIFY_NEW="1" LOGIND_NOTIFY_NEW="1"
@@ -74,16 +53,11 @@ LOGIND_NOTIFY_FAILED="1"
LOGIND_SKIP_REMOTE="1" LOGIND_SKIP_REMOTE="1"
DAILY_REPORT_HOUR="9" DAILY_REPORT_HOUR="9"
# 0 = не слать report.daily.ssh с агента (суточный отчёт только из SAC) # IANA-зона для «9:00» и календарной даты отчёта; пусто = как у команды date (обычно локаль сервера, см. timedatectl)
DAILY_REPORT_ENABLED="1"
DAILY_REPORT_TZ="" DAILY_REPORT_TZ=""
DAILY_REPORT_TOP_IPS="5" DAILY_REPORT_TOP_IPS="5"
# IANA-зона для «🕐 Время» в Telegram/email. Пусто = как DAILY_REPORT_TZ, иначе зона процесса (часто UTC в systemd — см. README). # IANA-зона для «🕐 Время» в Telegram/email. Пусто = как DAILY_REPORT_TZ, иначе зона процесса (часто UTC в systemd — см. README).
NOTIFY_TZ="" NOTIFY_TZ=""
# Подпись «🖥️ Сервер» в уведомлениях: пусто = hostname (+ IPv4); иначе фиксированное имя (например prod-web-01).
# SERVER_DISPLAY_NAME="prod-web-01"
# Явный IPv4 для SAC payload host.ipv4. Пусто = автоопределение.
# SERVER_IPV4="10.10.20.31"
# Не чаще 1 успешного SSH (Accepted) в Telegram на пару user+IP за N секунд (разные port в журнале). 0 = только дедуп внутри одного прохода. # Не чаще 1 успешного SSH (Accepted) в Telegram на пару user+IP за N секунд (разные port в журнале). 0 = только дедуп внутри одного прохода.
SSH_ACCEPT_NOTIFY_DEDUP_SEC="5" SSH_ACCEPT_NOTIFY_DEDUP_SEC="5"
@@ -92,11 +66,6 @@ MAX_ATTEMPTS="3"
BAN_CHECK_INTERVAL="60" BAN_CHECK_INTERVAL="60"
MONITOR_INTERVAL="10" MONITOR_INTERVAL="10"
# --- ipset (скрипт в этой папке): имена наборов и лимит элементов. Пусто в конфиге = значения по умолчанию из скрипта.
# IPSET_NAME_V4="ssh_monitor_ban"
# IPSET_NAME_V6="ssh_monitor_ban_v6"
# IPSET_MAXELEM="1048576"
# Окно и порог для уведомления о возможном массовом брутфорсе (journalctl) # Окно и порог для уведомления о возможном массовом брутфорсе (journalctl)
BRUTE_WINDOW_SEC="900" BRUTE_WINDOW_SEC="900"
BRUTE_MIN_FAILS="30" BRUTE_MIN_FAILS="30"
@@ -108,16 +77,7 @@ PROMETHEUS_TEXTFILE_DIR=""
# JSON-снимок состояния на каждой итерации (timestamp, hostname, dry_run). Пусто — отключено. # JSON-снимок состояния на каждой итерации (timestamp, hostname, dry_run). Пусто — отключено.
HEALTHCHECK_STATUS_FILE="" HEALTHCHECK_STATUS_FILE=""
# --- Автообновление (update_ssh_monitor.sh; читается updater'ом, не агентом) --- WATCHDOG_MAX_HEARTBEAT_AGE="5400"
# Пусто = ветка GIT_BRANCH (main). Для прода рекомендуется тег релиза, напр. 2.2.0-SAC.
# GIT_REF="2.2.0-SAC"
# off | tag | commit — см. docs/auto-update.ru.md. Дефолт в скрипте: off (совместимость).
# GIT_VERIFY_MODE="tag"
# 1 = разрешить git reset --hard при расхождении истории (только ручной/CI; риск).
# GIT_ALLOW_RESET="0"
# Порог устаревания heartbeat для watchdog (сек): интервал + 1 ч запас (240 мин → 18000).
WATCHDOG_MAX_HEARTBEAT_AGE="18000"
WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log" WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log"
WATCHDOG_SERVICE_NAME="ssh-monitor.service" WATCHDOG_SERVICE_NAME="ssh-monitor.service"
WATCHDOG_NOTIFY_ON_RECOVERY="0" WATCHDOG_NOTIFY_ON_RECOVERY="0"
-14
View File
@@ -1,14 +0,0 @@
[Unit]
Description=SSH Monitor with Telegram alerts and SAC integration
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ssh-monitor
Restart=always
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
2.2.2-SAC