Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
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 (Russian). |
System packages and dependencies (install on the server before running): step-by-step in docs/install-prerequisites.ru.md (Russian).
Configuration
The script reads /etc/ssh-monitor.conf in KEY="value" format.
- Copy the example:
sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.conf
- Restrict permissions:
sudo chmod 600 /etc/ssh-monitor.conf
- 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_TOKENandTELEGRAM_CHAT_ID; - or configure SMTP using the variables below with
NOTIFY_ORDER=""(auto-selected channels).
- for Telegram:
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_IDNOTIFY_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 inssh-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 allNOTIFY_CHAINchannels 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_FILEENABLE_LOGIND_MONITOR,LOGIND_NOTIFY_NEW,LOGIND_NOTIFY_REMOVED,LOGIND_NOTIFY_FAILED,LOGIND_SKIP_REMOTE— see «systemd-logind monitoring» belowDAILY_REPORT_HOUR(0..23),DAILY_REPORT_TZ(optional),NOTIFY_TZ(optional),SERVER_DISPLAY_NAME(optional),SSH_ACCEPT_NOTIFY_DEDUP_SEC,DAILY_REPORT_TOP_IPSBRUTE_WINDOW_SEC,BRUTE_MIN_FAILS,BRUTE_NOTIFY_COOLDOWN_SECPROMETHEUS_TEXTFILE_DIR— directory forssh_monitor.prom(node_exporter textfile collector)HEALTHCHECK_STATUS_FILE— path to JSON with last loop iteration markerENABLE_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_INTERVALWHITELIST_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 toLOG_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-configand--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 foremailchannel (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 reportYYYY-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— lastsystemd-logindjournal processing (Unix time); ~30 minutes of history on first run.BAN_LIST_FILE— ban state file (IP, ban end time, metadata).ENABLE_LOGIND_MONITOR—1enablesjournalctl -u systemd-logind,0disables this block.LOGIND_NOTIFY_NEW—1send Telegram on new logind session (New session … of user …).LOGIND_NOTIFY_REMOVED—1send Telegram on session end (Removed session …); default0(log only to avoid spam).LOGIND_NOTIFY_FAILED—1send Telegram on logind lines containing failed (broad filter; disable if needed).LOGIND_SKIP_REMOTE—1(default): for a new logind session, ifloginctlis available, skip duplicate Telegram when already covered bymonitor_ssh:Type=sshorServicepoints to sshd (oftenType=tty+Service=sshd); short retries ifloginctlreturns empty. Set0for 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 matchestimedatectl//etc/localtime). If the unit setsTZ=UTC, withoutDAILY_REPORT_TZthe report uses UTC — set an explicit zone in config.NOTIFY_TZ— IANA zone for «🕐 Время» lines in Telegram/email. Empty — useDAILY_REPORT_TZif set, else process zone. Common mismatch:ssh-monitor.servicehasEnvironment=TZ=UTC— setNOTIFY_TZorDAILY_REPORT_TZto your locale, e.g.Asia/Vladivostok.SERVER_DISPLAY_NAME— label in «🖥️ Server» line on every alert. Empty —hostnameplus primary IPv4 when available. Set a fixed name when many hosts share one Telegram chat. Details: docs/notifications.ru.md (Russian).SERVER_IPV4— optional override for SAChost.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.0disables this cross-iteration dedup (in-pass dedup byuser|ip|portremains).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 viajournalctl.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, writesssh_monitor.promwithssh_monitor_last_loop_unixtimeeach 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 (defaultssh-monitor.service).WATCHDOG_NOTIFY_ON_RECOVERY—1enables watchdog “OK” messages when healthy,0disables.
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-logindunit messages viajournalctl(-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. loginctlis used forLOGIND_SKIP_REMOTE: Type and Service decide whether to avoid duplicate Telegram withmonitor_ssh(seeLOGIND_SKIP_REMOTEabove).monitor_ssh: sshd events mostlyjournalctl _COMM=sshd(fallback unitssshd/ssh), linessort -u,Accepteddedup per user|IP|port within one pass; between passesSSH_ACCEPT_NOTIFY_DEDUP_SECfor 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.
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
hostnameandip(see prerequisites doc). - Optional
SERVER_DISPLAY_NAMEin config for a fixed label. - Message types and examples: 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 (Russian).
Run modes
Normal run (root):
sudo bash ./ssh-monitor
Check config and syntax without starting the loop, without changing iptables, no mandatory Telegram send:
sudo bash ./ssh-monitor --check-config
Dry-run (firewall rules unchanged; notifications go to stderr instead of sending):
sudo bash ./ssh-monitor --dry-run
Syntax check
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:
make dist
Produces ssh-monitor-<version>.tar.gz (via git archive). Prebuilt archives are attached to GitHub releases.
Installing the script, config, and systemd
- Install the script:
sudo install -m 750 ./ssh-monitor /usr/local/bin/ssh-monitor
- Ensure config exists (see Configuration):
sudo cp ./ssh-monitor.conf.example /etc/ssh-monitor.confsudo chmod 600 /etc/ssh-monitor.conf
- Create
/etc/systemd/system/ssh-monitor.service:
[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
- Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable ssh-monitor
sudo systemctl start ssh-monitor
- Status and logs:
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.
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.