104 Commits

Author SHA1 Message Date
PTah 6df83f1838 feat: release 2.2.1-SAC — phase 2 manifest verify and quiet SAC updates
Pinned GIT_REF/verify, release manifest, no blind reset --hard; suppress
lifecycle and watchdog Telegram during updater; healthcheck via json.dumps.
2026-07-08 11:48:34 +10:00
PTah 275d305d62 feat: watchdog Telegram shows server name (2.1.9-SAC)
Add shared server label helpers in ssh-monitor-perms.sh; roadmap marks
phase 1 done and phase 2.2.0 as next release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 11:28:57 +10:00
PTah 2048162057 feat: release 2.1.8-SAC — security phase 1 and SAC-first update hardening
Updater applies chmod/chown on config, spool and state paths on every run.
SAC is the primary update path; optional cron uses the same behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 09:44:24 +10:00
PTah ec0857b8f2 docs: security roadmap по аудиту (фазы 1–4, чек-лист) 2026-07-07 20:57:45 +10:00
PTah 732f32559a fix: harden push-github-snapshot URL rewrite and README header 2026-07-07 20:28:59 +10:00
PTah 141a841f6f docs: sync README version 2.1.6-SAC; add push-github-snapshot.sh 2026-07-07 20:27:44 +10:00
PTah 9e756674d0 fix: daily report без локального Telegram при fallback SAC (2.1.6-SAC)
Суточные report.daily.* уходят только в SAC/spool; SAC_TIMEOUT_SEC=45;
приоритет report.* в sac_flush_spool, до 50 файлов за цикл.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 12:00:35 +10:00
PTah 0a80effd77 docs: sync README version with version.txt 2.1.5-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:14:43 +10:00
PTah de61c6c771 fix: daily report active users count matches who sessions (2.1.5-SAC)
Include local/console who lines; active_count from session list not raw who line count.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 14:41:52 +10:00
PTah c96e870542 feat: enrich SAC events with session_id, tty and source_ip (2.1.4-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:13:40 +10:00
PTah 489d071208 feat(deploy): sync SERVER_IPV4 hint and DAILY_REPORT_ENABLED on update (2.1.3-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:01:16 +10:00
PTah 1361fd264c feat: merge missing SAC keys into config on deploy (2.1.2-SAC)
Append absent SAC variables from example instead of overwriting ssh-monitor.conf; set UseSAC=fallback on install and update.
2026-06-21 12:20:33 +10:00
PTah 65da13363d docs: 2.1.1-SAC README and SAC REPO_URL override
Описано, что SSH-обновление из SAC передаёт REPO_URL/GIT_BRANCH из настроек агентов.
2026-06-20 20:03:58 +10:00
PTah 31fcc4a8a7 merge: kalinamall main with 2.1.1-SAC updater git fix 2026-06-20 19:29:05 +10:00
PTah 2ff60280ef fix: default updater git to kalinamall and honor REPO_URL from SAC (2.1.1-SAC)
Always sync kalinamall remote from REPO_URL; support GIT_BRANCH env for SAC-driven updates.
2026-06-20 19:28:14 +10:00
PTah 9118bbdd64 chore: add .cursorignore to gitignore 2026-06-20 19:20:34 +10:00
PTah cdb74393aa docs: concise README with SAC integration and version 2.1.0-SAC 2026-06-20 19:20:34 +10:00
PTah b6d9901f05 chore: bump version to 2.1.0-SAC 2026-06-20 19:20:34 +10:00
PTah 76fc865d22 chore: add .cursorignore to gitignore 2026-06-20 19:07:52 +10:00
PTah bffc7f3245 docs: concise README with SAC integration and version 2.1.0-SAC 2026-06-20 19:02:59 +10:00
PTah 9b454c3270 chore: bump version to 2.1.0-SAC 2026-06-19 23:42:12 +10:00
PTah 619cc9e1ac chore(github): remove kalinamall/papatramp mirror scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:57:03 +10:00
PTah 577db1da47 chore(docs): sync repository URLs for kalinamall mirror 2026-06-16 11:37:52 +10:00
PTah 2956d14d94 chore(docs): GitHub repo URLs, remove .cursor from git, mirror scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:36:51 +10:00
PTah 9a3855bf0a feat: deploy дописывает HEARTBEAT_INTERVAL при отсутствии в конфиге (2.0.6-SAC)
Явная проверка ключей в /etc/ssh-monitor.conf, блок 240 мин + watchdog; исправлен текст startup heartbeat.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 16:46:41 +10:00
PTah 4cd1a85b3e feat: send agent.heartbeat on service startup (2.0.5-SAC)
After agent.lifecycle on boot, send heartbeat so SAC marks host online until next interval.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 11:51:16 +10:00
PTah 42af3b1ae6 fix: sync heartbeat interval on every deploy run
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 09:26:03 +10:00
PTah 7e5cad0231 feat: install ssh-monitor-watchdog on agent update (2.0.4-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:41:47 +10:00
PTah e576671580 chore: bump release to 2.0.3-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:38:56 +10:00
PTah 908f8958a4 feat: heartbeat 4h via HEARTBEAT_INTERVAL_MINUTES (2.0.2-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:25:19 +10:00
PTah 59ba8cf811 chore: sync Cursor rules from CursorRules (version bump RDP/SSH split)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:31:56 +10:00
PTah 84b8dab561 chore: add Cursor rule to bump SSH_MONITOR_VERSION and version.txt on script changes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:29:23 +10:00
PTah 2bed9516ec chore: bump release to 2.0.1-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:17:28 +10:00
PTah 3d0876bb0b feat: deploy UseSAC fallback by default on install and script upgrade
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:15:22 +10:00
PTah df77d8763f chore: bump release to 2.0.0-SAC
Major version line for NETLOGON deploy upgrades from 1.x agents.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 09:05:52 +10:00
PTah 79f5b664b2 feat: lifecycle notification_body and aligned host label 1.2.13-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 18:11:28 +10:00
PTah cf4e3b2e90 feat: lifecycle telegram_via routing and lifecycle details 1.2.12-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 12:26:14 +10:00
PTah e1a37b8f89 feat: notification source line in agent alerts (1.2.11-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 11:52:42 +10:00
PTah 70652bc6cb feat: Agent version line in daily SSH report (1.2.10-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 11:28:30 +10:00
PTah 192713d2da chore: release 1.2.9-SAC (align version.txt and SSH_MONITOR_VERSION)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 11:18:58 +10:00
PTah 454a220890 fix: daily report HTML line breaks without double spacing
Use single <br> in report_html and document DAILY_REPORT_ENABLED toggle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 09:52:09 +10:00
PTah 3820687310 feat: unify daily SSH report format with SAC; DAILY_REPORT_ENABLED toggle
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:17:59 +10:00
PTah eceac58a60 feat: include host.ipv4 in SAC payload and release 1.2.7-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 13:33:40 +10:00
PTah 3e1199e2ec chore: align ssh monitor and SAC module version 1.2.6-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 13:01:51 +10:00
PTah 8c542624eb feat: SERVER_DISPLAY_NAME в SAC host.display_name (sac-client 1.2.6-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 11:49:23 +10:00
PTah f06fbae8ee fix: update_ssh_monitor fetch+reset after kalinamall force-push
Release 1.2.5-SAC; prefer kalinamall remote; ff-only or hard reset.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:40:13 +10:00
PTah fe2a38857b chore: release 1.2.4-SAC — SAC ingest HTTP 201/409/202
Bump SSH_MONITOR_VERSION; sac-client release tag; updater logs sac-client ver.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:34:19 +10:00
PTah 0e83d8825d fix: accept SAC ingest HTTP 201, 409, and legacy 202 2026-05-28 09:13:13 +10:00
PTah 31c4265716 fix: log_action to stderr so captured paths stay valid
find_remote_ssh_monitor_path output was polluted by log_action on stdout,
breaking -f checks; mirror user messages to stderr instead.
2026-05-28 08:24:09 +10:00
PTah 731c50b0bc fix: find ssh-monitor in clone via git checkout and broader search
Restore ssh-monitor from HEAD when missing on disk; try git ls-files and
find fallback; log clone root listing on failure.
2026-05-28 08:20:37 +10:00
PTah 7f4d25b91c fix: sync /opt/scripts/update_ssh_monitor.sh from git clone each run
After git pull, copy updater from clone to DEPLOY_INSTALL_PATH and re-exec
once so cron always picks up new logging without manual cp.
2026-05-28 08:15:51 +10:00
PTah f3b7c9d41c feat: verbose update_ssh_monitor action log and run summary
Log each step to /var/log/update_script.log and stdout (unless --quiet).
End-of-run summary explains skip reasons: checksum match, git pull fail,
missing clone path, versions and sha256 prefixes.
2026-05-28 08:13:24 +10:00
PTah d179b360f2 fix: heartbeat only to SAC, never Telegram/email
agent.heartbeat bypasses NOTIFY_CHAIN in all UseSAC modes; SAC ingest only.
Bump to 1.2.3-SAC.
2026-05-28 07:56:18 +10:00
PTah dd980f0ba6 chore: bump release to 1.2.2-SAC
Increase SSH monitor release version used by sac-client payloads and refresh
RU/EN README references to the new version.
2026-05-27 15:53:29 +10:00
PTah 746a8f8f76 feat: bootstrap SAC config on update from pre-SAC installs
ssh-monitor.conf.example ships dual SAC + Telegram defaults; updater
overwrites /etc/ssh-monitor.conf when UseSAC is off or SAC_API_KEY empty
or sac-client.sh is missing, with backup. Leaves configured hosts untouched.
2026-05-27 15:23:35 +10:00
PTah 49ea1d2a7d chore: bump version to 1.2.1-SAC 2026-05-27 14:44:03 +10:00
PTah 29c91ee0db feat: send full daily report body and stats in SAC details 2026-05-27 14:15:13 +10:00
PTah 8627fdbcec chore: bump agent version to 1.2.0-SAC 2026-05-27 14:09:35 +10:00
PTah a4ea24c3f1 feat: SAC fallback failure counter (2.3)
After SAC_FALLBACK_FAILURES consecutive POST errors, skip SAC until
/health recovers; persist count in SAC_FAIL_COUNT_FILE.
2026-05-27 13:36:19 +10:00
PTah 3a1c9655ca feat: exclusive pilot — spool flush and verify script
Replay spool on each monitor loop; sac_post_payload refactor;
pilot-verify-exclusive.sh and --check-sac hints for exclusive mode.
2026-05-27 13:25:47 +10:00
PTah f1c4adcc13 feat: first deploy via update_ssh_monitor.sh --deploy
Add first_deploy.sh wrapper and --deploy mode: git clone, install
ssh-monitor and sac-client, optional config/systemd/updater copy,
console summary and config variable checklist.
2026-05-27 12:36:02 +10:00
PTah 1928e68088 fix: POST ingest to /api/v1/events when SAC_URL is host root
Health already used sac_base_url(); ingest used raw SAC_URL and got
HTTP 405 when config had only https://host without /api/v1/events.
2026-05-27 12:22:10 +10:00
PTah 4cb158f30a fix: always sync sac-client.sh in update_ssh_monitor
Copy sac-client.sh on every git pull by checksum, not only when
ssh-monitor binary changes. Restart service when either file updates.
2026-05-27 12:20:14 +10:00
PTah 18562af07a feat: map security events to SAC typed payloads 2026-05-27 11:24:19 +10:00
PTah 52654e2735 feat: sudo и notify_send шлют события в SAC (dual) 2026-05-27 11:01:47 +10:00
PTah a58027ed9b fix: update_ssh_monitor копирует sac-client.sh, REPO kalinamall 2026-05-27 10:47:38 +10:00
PTah ce48008738 merge: kalinamall main + UseSAC sac-client 2026-05-27 10:40:03 +10:00
PTah dd971d80bc feat: UseSAC sac-client.sh and --check-sac 2026-05-26 21:46:35 +10:00
PTah 2c65edf7db sync: align internal repo with 1.1.3-server-label
- REPO_URL points to git.kalinamall.ru

- ssh-monitor.conf may be committed here (secrets); .env still ignored

- Docs note for closed vs public GitHub
2026-05-26 19:34:01 +10:00
PTah 44f8eb6103 chore: ignore production config files with secrets
ssh-monitor.conf and local overrides must not be committed to public GitHub.
2026-05-26 19:33:39 +10:00
PTah b801cf2385 feat: подпись сервера во всех уведомлениях и документация
Добавлены message_ensure_server_line и SERVER_DISPLAY_NAME; версия 1.1.3-server-label.
Гайды по уведомлениям и автообновлению.
2026-05-25 09:23:32 +10:00
PapaTramp e2c0d5cbc7 Update update_ssh_monitor.sh 2026-05-14 12:59:12 +10:00
PapaTramp 3d20498515 Create update_ssh_monitor.sh
Скрипт проверки наличия обновлений основного скрипта
2026-05-14 12:57:55 +10:00
Andrey Lutsenko 48d5a0aeba Remove rutube artifacts from repo; ignore local downloader files 2026-05-03 19:51:00 +10:00
Andrey Lutsenko cde5b8c135 Add rutube-download helper and yt-dlp archive id list 2026-05-03 19:48:28 +10:00
Andrey Lutsenko 654822304e docs: English readme, MIT license, README cleanup
- Add readme_eng.md (parallel to README.md)
- Add LICENSE (MIT)
- Refresh README (version, repo table, license section)
2026-05-03 19:44:45 +10:00
ssh-monitor 2cf6c145ab Add ENABLE_IP_BAN mode switch
Introduce ENABLE_IP_BAN in config and script.
When set to 0, ssh-monitor works in monitor-only mode (alerts without ipset/iptables bans).
2026-04-16 12:13:27 +10:00
ssh-monitor b5b37b7e84 Переход автобана на ipset, документация и правки README
- ssh-monitor: баны через ipset (hash:ip timeout), одно правило iptables/ip6tables;
  нормализация бан-листа (один IP — одна строка), версия 1.1.1-ipset.
- docs/install-prerequisites.ru.md: зависимости и установка пакетов (в т.ч. ipset).
- README: ссылка на docs, установка скрипта/systemd без дублирования apt; пометка про ipset.
- IPSET-NOTES.txt: заметки по развёртыванию ipset.
- ssh-monitor.conf.example: закомментированные переменные IPSET_*.
2026-04-16 11:30:04 +10:00
ssh-monitor 074032b761 README: абзац с ключевыми темами для поиска на GitHub. 2026-04-14 14:16:08 +10:00
ssh-monitor 85730bafb0 Версия 1.0.0: релиз, Makefile (make dist), README и .gitignore для архива. 2026-04-14 13:52:26 +10:00
ssh-monitor e273c995a5 Удалена интеграция Zabbix; остались Telegram и email (SMTP). 2026-04-14 12:20:04 +10:00
ssh-monitor 5f9abc5e4c Zabbix: передать ZABBIX_SERVER/HOST_NAME в окружение python3 для trapper 2026-04-14 11:47:25 +10:00
ssh-monitor 6ff93d9552 Zabbix: короткий plain для канала; seq в файле; alert через python3 -o 2026-04-14 11:41:03 +10:00
ssh-monitor 7d8cb00689 Zabbix: усечение UTF-8 для trapper Character (ZABBIX_ALERT_MAX_BYTES) 2026-04-14 11:29:45 +10:00
ssh-monitor c83b4f19d1 Zabbix alert: убрать NUL из текста перед trapper (-i) 2026-04-14 11:15:19 +10:00
ssh-monitor 54e90672c8 Zabbix: успех только по failed:0 в ответе; seq через -k/-o; подсказка при отказе сервера 2026-04-14 11:10:30 +10:00
ssh-monitor 7e83dc6a83 Zabbix: логировать stdout+stderr zabbix_sender; успех при failed: 0 в ответе 2026-04-14 11:06:11 +10:00
ssh-monitor 190add949d Zabbix: два вызова zabbix_sender (alert и seq), лог stderr при ошибке 2026-04-14 10:57:32 +10:00
ssh-monitor 27219355c2 fix(notify): отправка во все каналы NOTIFY_ORDER, не только первый успех
- BACKUP_WEBHOOK только если все каналы неуспешны

- README/conf.example; версия 0.9.5.3
2026-04-14 10:50:38 +10:00
ssh-monitor 98f63e77ad 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
2026-04-14 10:23:58 +10:00
ssh-monitor fa3d415fd9 chore: версия 0.9.5.1 2026-04-14 10:14:22 +10:00
ssh-monitor 06c8fe2e8e 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
2026-04-14 10:12:40 +10:00
ssh-monitor 95ff57bc4d feat: строка «Сервер» с основным IPv4 в старте/стопе, отчёте и heartbeat 2026-04-14 10:08:29 +10:00
ssh-monitor 79233f52f7 fix: версия в Telegram при старте сразу под заголовком 2026-04-14 10:06:02 +10:00
ssh-monitor e9c98d4647 docs: обязательный NOTIFY_CHAIN в README и ssh-monitor.conf.example 2026-04-14 09:56:18 +10:00
ssh-monitor a3bd7ed9ad fix: выход с ошибкой, если нет ни одного канала NOTIFY_CHAIN 2026-04-14 09:55:03 +10:00
ssh-monitor a90efd3ed8 docs: Zabbix и пояснение ZABBIX_SEQ_KEY в README 2026-04-14 09:52:07 +10:00
ssh-monitor 36dddb35d6 feat(notify): каскад telegram → zabbix → email, NOTIFY_ORDER, SMTP, v0.9.5 2026-04-14 09:44:43 +10:00
PTah 85c18be12e chore: зафиксировать линию версий 0.9 (semver 0.9.0)
- Комментарий в скрипте и уточнение в README
2026-04-14 08:11:49 +10:00
PTah f9e033777e feat: версия 0.9.0, --version и вывод в лог/health/старт
- SSH_MONITOR_VERSION + суффикс +git[-dirty] при запуске из репозитория

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

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

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

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

- README: параметры logind, ротация LAST_*, journalctl
2026-04-13 09:45:41 +10:00
24 changed files with 4687 additions and 231 deletions
+11 -1
View File
@@ -1,4 +1,14 @@
.DS_Store
.DS_Store
CalculatorApp/
.cursor/
# .env — не коммитить; ssh-monitor.conf с секретами допустим только в закрытом зеркале (не в публичном GitHub)
.env
.env.*
# Артефакты локальной сборки (релизные архивы грузятся в GitHub Releases, в git не коммитим)
ssh-monitor-*.tar.gz
# Локальный загрузчик Rutube / yt-dlp — живёт в другом репозитории
rutube-download.py
.ytdl-archive.txt
.cursorignore
+24
View File
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,21 @@
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.
+4 -1
View File
@@ -1,10 +1,13 @@
# Версия берётся из ssh-monitor (SSH_MONITOR_VERSION).
.PHONY: dist clean-dist
.PHONY: dist clean-dist manifest
VERSION := $(shell sed -n 's/^SSH_MONITOR_VERSION="\(.*\)".*/\1/p' ssh-monitor | head -n1)
DIST := ssh-monitor-$(VERSION).tar.gz
dist: $(DIST)
manifest:
./scripts/build-release-manifest.sh
$(DIST): ssh-monitor
git archive --format=tar.gz --prefix=ssh-monitor-$(VERSION)/ -o $(DIST) HEAD
+70 -43
View File
@@ -1,9 +1,27 @@
# ssh-monitor
# ssh-monitor
Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`systemd-logind`** (локальные/графические сессии и др.) с уведомлениями в **Telegram** и по **электронной почте (SMTP)**, опциональным резервным webhook, авто-блокировкой IP (IPv4 через `iptables`, IPv6 через `ip6tables` при наличии) и ежедневным отчётом.
**Версия:** `2.2.1-SAC`
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"`.
1. Скопируйте пример:
@@ -21,13 +39,14 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`
- `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** (см. таблицу каналов ниже)
- `BACKUP_WEBHOOK_URL` — резервная доставка JSON `{"text":"..."}` (например Slack Incoming Webhook), если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить сообщение; **не заменяет** обязательность хотя бы одного основного канала (см. шаг 3)
- `BACKUP_WEBHOOK_URL` *(deprecated)* резервная доставка JSON, если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить; удаление в 2.3.x
- `LOG_FILE`, `LAST_HEARTBEAT_FILE`, `LAST_REPORT_FILE`, `LAST_SSH_CHECK_FILE`, `LAST_SUDO_CHECK_FILE`, `LAST_SECURITY_EVENTS_FILE`, `LAST_LOGIND_CHECK_FILE`, `BAN_LIST_FILE`
- `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — см. раздел «Мониторинг systemd-logind» ниже
- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS`
- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_ENABLED` (1/0 — отчёт с агента; 0 = только SAC), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SERVER_DISPLAY_NAME` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS`
- `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC`
- `PROMETHEUS_TEXTFILE_DIR` — каталог для `ssh_monitor.prom` (совместимость с node_exporter textfile collector)
- `HEALTHCHECK_STATUS_FILE` — путь к JSON-файлу с меткой последней итерации цикла
- `ENABLE_IP_BAN``1` (по умолчанию): мониторинг и автобан через ipset; `0`: только мониторинг и уведомления (без ipset/бана)
- `BAN_TIME`, `MAX_ATTEMPTS`, `BAN_CHECK_INTERVAL`, `MONITOR_INTERVAL`
- `WHITELIST_IPS`, `WHITELIST_SUBNETS` (CSV, например `ip1,ip2`)
- `WATCHDOG_MAX_HEARTBEAT_AGE`, `WATCHDOG_LOG_FILE`, `WATCHDOG_SERVICE_NAME`, `WATCHDOG_NOTIFY_ON_RECOVERY`
@@ -40,7 +59,7 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`
- `TELEGRAM_CHAT_ID` — ID чата/пользователя, куда отправляются уведомления.
- `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**; см. таблицу «настроен»).
- `BACKUP_WEBHOOK_URL`URL для резервной отправки (тело JSON `{"text":"..."}`); только fallback при сбое основных каналов, **не засчитывается** как «настроенный канал» (см. шаг 3).
- `BACKUP_WEBHOOK_URL`*(deprecated)* URL резервной отправки; только fallback; **не засчитывается** как основной канал
- `LOG_FILE` — путь к основному лог-файлу скрипта.
- `LAST_HEARTBEAT_FILE` — файл с timestamp последнего heartbeat-сообщения.
- `LAST_REPORT_FILE` — дата последнего ежедневного отчёта в формате `YYYY-MM-DD` (старый формат Unix-времени при первом запуске будет автоматически интерпретирован).
@@ -55,8 +74,11 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`
- `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-сессий.
- `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** — тогда задайте явную зону в конфиге.
- `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`).
- `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа.
- `BRUTE_WINDOW_SEC` — окно (секунды) для оценки «массового» брутфорса по `journalctl`.
@@ -64,7 +86,8 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`
- `BRUTE_NOTIFY_COOLDOWN_SEC` — пауза между повторными уведомлениями по одному и тому же IP.
- `PROMETHEUS_TEXTFILE_DIR` — если задан существующий каталог, на каждой итерации пишется `ssh_monitor.prom` с метрикой `ssh_monitor_last_loop_unixtime`.
- `HEALTHCHECK_STATUS_FILE` — если задан, на каждой итерации обновляется JSON `{ "ts", "hostname", "dry_run" }`.
- `BAN_TIME` — длительность бана IP в секундах.
- `ENABLE_IP_BAN` — включить автобан IP (`1`) или работать только как монитор с уведомлениями (`0`; **ipset** не требуется).
- `BAN_TIME` — длительность бана IP в секундах (используется при `ENABLE_IP_BAN=1`).
- `MAX_ATTEMPTS` — число неудачных SSH-попыток до автоматического бана.
- `BAN_CHECK_INTERVAL` — интервал проверки просроченных банов и аудита согласованности бан-листа с firewall (секунды).
- `MONITOR_INTERVAL` — пауза между итерациями основного цикла мониторинга (секунды).
@@ -96,6 +119,24 @@ Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`
Команда **`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):
@@ -124,22 +165,34 @@ bash -n ./ssh-monitor
## Релизный архив
Версия задаётся в скрипте переменной **`SSH_MONITOR_VERSION`** (текущая стабильная — **1.0.0**). Сборка tarball из текущего git-дерева:
Версия задаётся в **`SSH_MONITOR_VERSION`** в файле **`ssh-monitor`** и дублируется в **`version.txt`** (та же строка, что в шапке README). Модуль **`sac-client.sh`** использует ту же версию для SAC `product_version`. Сборка tarball из текущего git-дерева:
```bash
make dist
```
Появится файл `ssh-monitor-<версия>.tar.gz` (через `git archive`). Готовые архивы для установки без клона репозитория прикладываются к [релизам на GitHub](https://github.com/PTah/ssh-monitor/releases).
Появится файл `ssh-monitor-<версия>.tar.gz` (через `git archive`). Готовые архивы для установки без клона репозитория прикладываются к [релизам на GitHub](https://git.kalinamall.ru/PapaTramp/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. Скопируйте скрипт в постоянное место:
- `sudo install -m 750 ./ssh-monitor /usr/local/bin/ssh-monitor`
2. Убедитесь, что конфиг есть:
- `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf` (если еще не создан)
- `sudo install -m 644 ./sac-client.sh /usr/local/bin/sac-client.sh`
2. Убедитесь, что конфиг создан (см. раздел **«Конфигурация»** выше):
- `sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf`
- `sudo chmod 600 /etc/ssh-monitor.conf`
3. Создайте unit-файл `/etc/systemd/system/ssh-monitor.service`:
3. Создайте unit-файл `/etc/systemd/system/ssh-monitor.service` (или `sudo cp ssh-monitor.service.example ...`):
```ini
[Unit]
@@ -177,40 +230,14 @@ sudo journalctl -u ssh-monitor -f
Пример для `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`, если задан).
- активен ли `ssh-monitor.service`;
- не устарел ли heartbeat (`LAST_HEARTBEAT_FILE`).
Пошаговая установка скрипта **ssh-monitor-watchdog** и **timer** в **systemd**: раздел 6 в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md).
Если сервис неактивен или heartbeat старше `WATCHDOG_MAX_HEARTBEAT_AGE`, watchdog выполняет `systemctl restart` и отправляет уведомление (Telegram и при сбое — `BACKUP_WEBHOOK_URL`, если задан).
**Ключевые темы (для поиска):** мониторинг **SSH** и **sshd**, **bash**-скрипт для **Linux**-сервера, уведомления в **Telegram** и по **SMTP** / электронной почте, **systemd-logind**, **sudo**, **journalctl**, **iptables** / **ip6tables**, автоматический **бан IP** и **whitelist**, **systemd** unit, **ежедневный отчёт**, **heartbeat**, **OpenSSH**, безопасность сервера, **Prometheus** textfile, опционально **watchdog** для сервиса.
### Установка watchdog
## Лицензия
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
```
Проект распространяется по лицензии **MIT** — см. файл [LICENSE](LICENSE).
+174
View File
@@ -0,0 +1,174 @@
# Автообновление 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_commit` в manifest совпал с HEAD):
```bash
git commit -m "release: 2.2.0-SAC"
./scripts/build-release-manifest.sh 2.2.0-SAC
git add release/manifest-2.2.0-SAC.json && git commit -m "release: manifest 2.2.0-SAC"
git tag 2.2.0-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
@@ -0,0 +1,152 @@
# Системные зависимости и установка окружения
Этот документ описывает, **какой софт** на сервере нужен для работы **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
@@ -0,0 +1,98 @@
# Уведомления ssh-monitor
Документ описывает, **какие сообщения** отправляет монитор, **какие поля** в них есть и как настроить **подпись сервера** и **часовой пояс**.
См. также: [README.md](../README.md), пример конфига [ssh-monitor.conf.example](../ssh-monitor.conf.example).
## Подпись сервера (🖥️ Сервер)
Начиная с версии **1.1.3-server-label**, каждое уведомление, проходящее через **`notify_send()`**, получает строку **«🖥️ Сервер: …»** сразу **после первой строки** (заголовка алерта).
Формат значения:
| Условие | Пример |
|---------|--------|
| `SERVER_DISPLAY_NAME` пусто, IPv4 найден | `web01 (10.1.20.5)` |
| `SERVER_DISPLAY_NAME` пусто, IPv4 нет | `web01` |
| Задан `SERVER_DISPLAY_NAME="prod-db-01"` | `prod-db-01 (10.1.20.5)` |
IPv4 определяется через `ip -4 route get` (исходящий адрес) или `hostname -I`. Нужны утилиты **`hostname`** и **`ip`** — см. [install-prerequisites.ru.md](install-prerequisites.ru.md).
Если в тексте сообщения уже есть подстрока **`🖥️ Сервер:`**, вторая строка **не добавляется** (защита от дубля при ручной вставке).
### Пример: успешный SSH
```
✅ УСПЕШНОЕ SSH ПОДКЛЮЧЕНИЕ
🖥️ Сервер: myhost (10.1.20.5)
👤 Пользователь: alice
🌐 IP адрес: 10.1.20.1
🕐 Время: 25.05.2026 09:08:22
```
### Пример: sudo
```
⚠️ ИСПОЛЬЗОВАНИЕ SUDO
🖥️ Сервер: myhost (10.1.20.5)
👤 Пользователь: alice
🔑 От имени USER: root
💻 Команда: /bin/bash
📁 Директория: /home/alice
🕐 Время: 24.05.2026 16:54:49
```
## Настройка в `/etc/ssh-monitor.conf`
```bash
# Пусто = hostname (+ IPv4 при возможности)
SERVER_DISPLAY_NAME=""
# Или фиксированное имя для этого хоста в общем чате:
# SERVER_DISPLAY_NAME="dc1-app-03"
```
Проверка эффективной подписи:
```bash
sudo ssh-monitor --check-config
```
В выводе будет строка `SERVER_DISPLAY_NAME=... (эффективно: ...)`.
## Типы уведомлений
| Событие | Заголовок (первая строка) | Доп. поля |
|---------|----------------------------|-----------|
| Успешный SSH | ✅ УСПЕШНОЕ SSH ПОДКЛЮЧЕНИЕ | пользователь, IP клиента, время |
| SSH под root | 🔑 SSH ВХОД ПОД ROOT | IP, время |
| Неудачная попытка SSH | ❌ НЕУДАЧНАЯ ПОПЫТКА SSH | пользователь, IP, счётчик попыток, время |
| Sudo | ⚠️ ИСПОЛЬЗОВАНИЕ SUDO | пользователь, USER=, команда, PWD, время |
| Бан IP | 🚫 IP ЗАБЛОКИРОВАН АВТОМАТИЧЕСКИ | IP, попытки, длительность, время |
| Порог без бана | ⚠️ ЛИМИТ НЕУДАЧНЫХ SSH (автобан отключён…) | IP, попытки, время |
| Брутфорс | 🧨 ВОЗМОЖНЫЙ МАССОВЫЙ БРУТФОРС SSH | IP, счётчик за окно, время |
| Host key / MITM | 🔐 ВНИМАНИЕ: изменение SSH host key… | фрагмент журнала, время |
| logind: новая сессия | 🖥️ НОВАЯ СЕССИЯ (systemd-logind) | пользователь, ID сессии, время |
| logind: сбой | ❌ СБОЙ (systemd-logind) | строка журнала, время |
| Ежедневный отчёт | 📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА | статистика 24 ч, баны, топ IP, сессии |
| Heartbeat | ❤️ Heartbeat - скрипт мониторинга работает | время |
| Старт / стоп | ✅ СКРИПТ МОНИТОРИНГА ЗАПУЩЕН / ⚠️ ОСТАНОВЛЕН | версия, каналы, время |
Во всех перечисленных случаях **🖥️ Сервер** добавляется автоматически (кроме сообщений, где вы сами вставили эту строку).
## Время в сообщениях (🕐)
Строки **«🕐 Время»** формируются функцией **`notification_date()`**:
1. Если задан **`NOTIFY_TZ`** (IANA, например `Europe/Moscow`) — используется он.
2. Иначе, если задан **`DAILY_REPORT_TZ`** — он.
3. Иначе — зона процесса (`date` у systemd-сервиса; часто UTC, если в unit указано `Environment=TZ=UTC`).
Ежедневный отчёт по календарю и часу **`DAILY_REPORT_HOUR`** использует **`DAILY_REPORT_TZ`** (или зону процесса) — см. README.
## Каналы доставки
- **`NOTIFY_CHAIN`**: `telegram`, `email` — при каждом событии попытка **во все** каналы списка.
- **`BACKUP_WEBHOOK_URL`** *(deprecated, legacy)*: только если **все** каналы цепочки не доставили сообщение. В типовом деплое не используется; удаление в 2.3.x.
Режим **`--dry-run`**: уведомления не отправляются; в stderr печатается текст **уже с подписью сервера**.
+258
View File
@@ -0,0 +1,258 @@
# 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
@@ -0,0 +1,5 @@
#!/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
@@ -0,0 +1,220 @@
# 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
@@ -0,0 +1,11 @@
{
"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
@@ -0,0 +1,11 @@
{
"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"
}
}
+520
View File
@@ -0,0 +1,520 @@
# 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
}
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# Генерация release/manifest-VERSION.json (SHA256 файлов + git commit HEAD).
# Запуск из корня репозитория: ./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
[ -f "$f" ] || {
echo "ERROR: нет файла $f" >&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
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_file(path: str) -> str:
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:
path = os.path.join(root, name)
manifest["files"][name] = f"sha256:{sha256_file(path)}"
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
@@ -0,0 +1,41 @@
#!/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
@@ -0,0 +1,156 @@
#!/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)"
+713 -171
View File
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
#!/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
}
+18 -1
View File
@@ -3,9 +3,14 @@ set -Eeuo pipefail
IFS=$'\n\t'
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"
LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt"
WATCHDOG_MAX_HEARTBEAT_AGE=5400
WATCHDOG_MAX_HEARTBEAT_AGE=18000
WATCHDOG_SERVICE_NAME="ssh-monitor.service"
WATCHDOG_NOTIFY_ON_RECOVERY="0"
@@ -62,6 +67,7 @@ PY
notify_send() {
local message="$1"
message="$(ssh_monitor_message_ensure_server_line "$message")"
if send_telegram_raw "$message"; then
return 0
fi
@@ -75,9 +81,15 @@ notify_send() {
load_config() {
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
source "$CONFIG_FILE"
fi
if [ -n "${BACKUP_WEBHOOK_URL:-}" ]; then
write_log "WARN: BACKUP_WEBHOOK_URL deprecated (legacy fallback); будет удалён в 2.3.x"
fi
}
is_service_active() {
@@ -130,6 +142,11 @@ main() {
mkdir -p "$(dirname "$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=""
if ! is_service_active; then
reason="systemd reported inactive service"
+49 -9
View File
@@ -1,11 +1,9 @@
TELEGRAM_BOT_TOKEN="replace_with_telegram_bot_token"
TELEGRAM_CHAT_ID="replace_with_telegram_chat_id"
# Закрытое зеркало — доверенный инстанс; в публичном GitHub используйте example без секретов.
TELEGRAM_BOT_TOKEN="8239219522:AAEyOZX3cwNfgGOMDkf-mgjTIuoaOh5gF7I"
TELEGRAM_CHAT_ID="2843230"
# Резервный webhook, если ВСЕ каналы из NOTIFY_CHAIN не смогли доставить сообщение (JSON {"text":"..."}).
# ВАЖНО: BACKUP_WEBHOOK_URL НЕ заменяет основной канал — без хотя бы одного из telegram/email
# скрипт сразу завершится с ошибкой в stderr: «Не настроен ни один канал отправки оповещений» (код 1),
# в том числе при --check-config и --dry-run. Сначала настройте Telegram или SMTP ниже.
BACKUP_WEBHOOK_URL=""
# DEPRECATED: legacy fallback webhook (Slack/Discord). Не используется в типовом деплое; удаление в 2.3.x.
# BACKUP_WEBHOOK_URL=""
# --- Очередь оповещений (NOTIFY_ORDER) ---
# CSV: telegram, email (или сокращения tg, mail).
@@ -25,6 +23,24 @@ BACKUP_WEBHOOK_URL=""
# NOTIFY_ORDER="email"
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.
MAIL_SMTP_HOST=""
MAIL_SMTP_PORT="587"
@@ -37,6 +53,8 @@ MAIL_SMTP_SSL="0"
LOG_FILE="/var/log/ssh_monitor.log"
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_SSH_CHECK_FILE="/var/log/last_ssh_check"
LAST_SUDO_CHECK_FILE="/var/log/last_sudo_check"
@@ -44,6 +62,9 @@ LAST_SECURITY_EVENTS_FILE="/var/log/last_security_events_check"
LAST_LOGIND_CHECK_FILE="/var/log/last_logind_check"
BAN_LIST_FILE="/var/log/ssh_bans.txt"
# 1 = мониторинг + автобан IP (нужен ipset). 0 = только мониторинг и уведомления (логины, неудачи, sudo, logind и т.д. — без ipset/iptables).
ENABLE_IP_BAN="1"
# systemd-logind: 1 = включено, 0 = выключено
ENABLE_LOGIND_MONITOR="1"
LOGIND_NOTIFY_NEW="1"
@@ -53,11 +74,16 @@ LOGIND_NOTIFY_FAILED="1"
LOGIND_SKIP_REMOTE="1"
DAILY_REPORT_HOUR="9"
# IANA-зона для «9:00» и календарной даты отчёта; пусто = как у команды date (обычно локаль сервера, см. timedatectl)
# 0 = не слать report.daily.ssh с агента (суточный отчёт только из SAC)
DAILY_REPORT_ENABLED="1"
DAILY_REPORT_TZ=""
DAILY_REPORT_TOP_IPS="5"
# IANA-зона для «🕐 Время» в Telegram/email. Пусто = как DAILY_REPORT_TZ, иначе зона процесса (часто UTC в systemd — см. README).
NOTIFY_TZ=""
# Подпись «🖥️ Сервер» в уведомлениях: пусто = hostname (+ IPv4); иначе фиксированное имя (например prod-web-01).
# SERVER_DISPLAY_NAME="prod-web-01"
# Явный IPv4 для SAC payload host.ipv4. Пусто = автоопределение.
# SERVER_IPV4="10.10.20.31"
# Не чаще 1 успешного SSH (Accepted) в Telegram на пару user+IP за N секунд (разные port в журнале). 0 = только дедуп внутри одного прохода.
SSH_ACCEPT_NOTIFY_DEDUP_SEC="5"
@@ -66,6 +92,11 @@ MAX_ATTEMPTS="3"
BAN_CHECK_INTERVAL="60"
MONITOR_INTERVAL="10"
# --- ipset (скрипт в этой папке): имена наборов и лимит элементов. Пусто в конфиге = значения по умолчанию из скрипта.
# IPSET_NAME_V4="ssh_monitor_ban"
# IPSET_NAME_V6="ssh_monitor_ban_v6"
# IPSET_MAXELEM="1048576"
# Окно и порог для уведомления о возможном массовом брутфорсе (journalctl)
BRUTE_WINDOW_SEC="900"
BRUTE_MIN_FAILS="30"
@@ -77,7 +108,16 @@ PROMETHEUS_TEXTFILE_DIR=""
# JSON-снимок состояния на каждой итерации (timestamp, hostname, dry_run). Пусто — отключено.
HEALTHCHECK_STATUS_FILE=""
WATCHDOG_MAX_HEARTBEAT_AGE="5400"
# --- Автообновление (update_ssh_monitor.sh; читается updater'ом, не агентом) ---
# Пусто = ветка 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_SERVICE_NAME="ssh-monitor.service"
WATCHDOG_NOTIFY_ON_RECOVERY="0"
+14
View File
@@ -0,0 +1,14 @@
[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
+1822
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
2.2.1-SAC