From b0abf99d1bd27d5d9e7aeeee9b9ffc7579939266 Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 8 Jul 2026 14:12:25 +1000 Subject: [PATCH] feat: suppress sac-fail.count on shutdown (2.3.2-SAC) Phase 4.2: SAC_SUPPRESS_FAIL_COUNT during lifecycle shutdown so restart during SAC deploy does not trip Telegram fallback. Add docs/sac-ingest.ru.md for SAC_TIMEOUT_SEC guidance, smoke test, and release manifest. --- README.md | 2 +- contrib/test-sac-fail-count.sh | 32 ++++++++++++++++++++++++++ docs/sac-ingest.ru.md | 40 +++++++++++++++++++++++++++++++++ docs/security-roadmap.ru.md | 5 +++-- release/manifest-2.3.2-SAC.json | 11 +++++++++ sac-client.sh | 12 ++++++++++ ssh-monitor | 3 ++- ssh-monitor-perms.sh | 2 ++ ssh-monitor.conf.example | 1 + version.txt | 2 +- 10 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 contrib/test-sac-fail-count.sh create mode 100644 docs/sac-ingest.ru.md create mode 100644 release/manifest-2.3.2-SAC.json diff --git a/README.md b/README.md index c4f35d4..a99eddd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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 — опционально. +**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). Ingest / таймауты / fallback: [docs/sac-ingest.ru.md](docs/sac-ingest.ru.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`). С **2.3.0** конфиг читается **без `source`** (whitelist ключей, без выполнения bash из файла). Updater дописывает обязательные ключи и **на каждом прогоне** применяет `chmod`/`chown` к конфигу, SAC-spool и state-файлам. Подробнее: [docs/security-roadmap.ru.md](docs/security-roadmap.ru.md). diff --git a/contrib/test-sac-fail-count.sh b/contrib/test-sac-fail-count.sh new file mode 100644 index 0000000..aee334a --- /dev/null +++ b/contrib/test-sac-fail-count.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Smoke-test sac-fail.count suppress on shutdown flag. Run from repo root. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +write_log() { :; } +ssh_monitor_secure_file() { :; } +ssh_monitor_secure_dir() { mkdir -p "$1"; } + +# shellcheck source=sac-client.sh +source "$ROOT/sac-client.sh" + +export SAC_FAIL_COUNT_FILE="${tmpdir}/sac-fail.count" + +sac_write_fail_count 0 +sac_increment_fail_count +[ "$(sac_read_fail_count)" = "1" ] || { + echo "FAIL: expected count 1 after increment" >&2 + exit 1 +} + +SAC_SUPPRESS_FAIL_COUNT=1 +sac_increment_fail_count || true +[ "$(sac_read_fail_count)" = "1" ] || { + echo "FAIL: count changed under SAC_SUPPRESS_FAIL_COUNT" >&2 + exit 1 +} + +echo "OK sac-fail.count suppress" diff --git a/docs/sac-ingest.ru.md b/docs/sac-ingest.ru.md new file mode 100644 index 0000000..a9ab2af --- /dev/null +++ b/docs/sac-ingest.ru.md @@ -0,0 +1,40 @@ +# SAC ingest: таймауты и fallback + +Документ для эксплуатации **ssh-monitor** с `UseSAC=fallback` или `exclusive` при нагрузке на SAC (массовый update агентов, deploy backend). + +См. также: [ingest-mass-update-backlog.ru.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/ingest-mass-update-backlog.ru.md) (SAC + ops). + +## `SAC_TIMEOUT_SEC` + +Таймаут **connect** и **max-time** для `curl` к SAC (`/health`, `POST /api/v1/events`). + +| Сценарий | Рекомендация | +|----------|--------------| +| Обычная нагрузка | **45** (дефолт в `ssh-monitor.conf.example` и при миграции с 12) | +| Массовый restart агентов, тяжёлый ingest на SAC | **60–90** на время окна обслуживания | +| SAC на том же хосте перезапускается (`sac-deploy`) | Кратковременные отказы POST — норма; не поднимать таймаут «навсегда» без причины | + +На стороне SAC (nginx) для ingest задан `proxy_read_timeout 120s` на `POST /api/v1/events`. Агентский таймаут **не обязан** равняться 120: достаточно, чтобы POST не обрывался раньше типичного ответа API. При `SAC_TIMEOUT_SEC` меньше nginx-лимита агент уйдёт в spool и `sac-fail.count`, а не «висит» минутами. + +Проверка после смены: + +```bash +sudo ssh-monitor --check-config +sudo ssh-monitor --check-sac +``` + +## `sac-fail.count` и режим `fallback` + +При ошибке POST ingest счётчик в `SAC_FAIL_COUNT_FILE` увеличивается. После **`SAC_FALLBACK_FAILURES`** подряд (по умолчанию 5) агент перестаёт слать в SAC и шлёт только в локальный Telegram, пока `/health` снова не станет OK. + +**Сброс счётчика:** любой успешный POST (в т.ч. heartbeat) обнуляет `sac-fail.count`. + +**Shutdown / SIGTERM (с 2.3.2-SAC):** при остановке монитора (`systemctl stop`, restart во время SAC-update) неудачный lifecycle POST **не увеличивает** счётчик — иначе после штатного restart пачки хостов агент ложно уходит в Telegram-fallback. + +Lifecycle при SAC-update по-прежнему **пропускается**, если существует state-file `agent-update-in-progress` (см. `update_ssh_monitor.sh`). + +## Операционные правила + +1. Не совмещать `sudo /opt/sac-deploy.sh` и массовое «Обновить ssh-monitor» по многим хостам в одно окно. +2. Обновлять Linux-хосты **пачками по 2–3**. +3. На зрелых хостах — **`UseSAC=exclusive`**, если локальный Telegram при fallback не нужен. diff --git a/docs/security-roadmap.ru.md b/docs/security-roadmap.ru.md index a69021e..8cc15cc 100644 --- a/docs/security-roadmap.ru.md +++ b/docs/security-roadmap.ru.md @@ -25,7 +25,8 @@ | **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.3.0-SAC** | **Фаза 3** | Парсер конфига без `source`, валидация, удаление BACKUP_WEBHOOK | ✅ реализовано | +| **2.3.2-SAC** | **Фаза 4.2** | Shutdown: не крутить `sac-fail.count`; docs `SAC_TIMEOUT_SEC` | ✅ реализовано | +| **2.3.1-SAC** | Hotfix | `--check-config` + whitelist IPv4 | ✅ реализовано | | **2.2.3-SAC** | Дополнение к 2.2.x | Подавление sudo TG при SAC bootstrap/update | ✅ реализовано | | **2.2.1-SAC** | Дополнение к 2.2.0 | Подавление lifecycle/watchdog TG при SAC-update | ✅ реализовано | | **2.2.0-SAC** | **Фаза 2** | Manifest, pinned ref, без слепого `reset --hard` | ✅ реализовано | @@ -33,7 +34,7 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом релизе. -**Текущий статус (2026-07-08):** фаза 3 закрыта (2.3.0-SAC). **Дальше — ingest backlog (SAC) и опционально GPG / GIT_REF=tag на проде.** +**Текущий статус (2026-07-08):** фаза **4.2** закрыта (2.3.2-SAC). Дальше — ops (`UseSAC=exclusive`, пачки update) и опционально GPG / `GIT_REF=tag` на проде. 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). --- diff --git a/release/manifest-2.3.2-SAC.json b/release/manifest-2.3.2-SAC.json new file mode 100644 index 0000000..ccaab35 --- /dev/null +++ b/release/manifest-2.3.2-SAC.json @@ -0,0 +1,11 @@ +{ + "version": "2.3.2-SAC", + "git_commit": "", + "files": { + "ssh-monitor": "sha256:ca64d1b2cfda29451704e43d6d5995dbdd5cac44f7e4d85f52d8f54429083c0f", + "sac-client.sh": "sha256:e3b561280d1c146f565a1fa73e4df29f07b4ffdf149c2b30074fac98722ee9b3", + "update_ssh_monitor.sh": "sha256:76c610ed229d510c41ddd0d10db2b68d48c08b89eb1c8252341ce85f088bf6cd", + "ssh-monitor-watchdog": "sha256:c03c50162b552a7790dd0a63eecec08d672ee04955281b5ec434ed92af5e0e2f", + "ssh-monitor-perms.sh": "sha256:19531b61ebac2e2f9a19fcd9fa40b6fca4651a979dfc6ab9a347a0398ce5e012" + } +} diff --git a/sac-client.sh b/sac-client.sh index 96a5199..3e49cb0 100644 --- a/sac-client.sh +++ b/sac-client.sh @@ -119,8 +119,20 @@ sac_reset_fail_count() { sac_write_fail_count 0 } +# Внутренний флаг: при shutdown/SIGTERM не крутить счётчик (restart во время SAC-deploy). +sac_fail_count_increment_suppressed() { + case "${SAC_SUPPRESS_FAIL_COUNT:-0}" in + 1 | yes | true | on) return 0 ;; + esac + return 1 +} + sac_increment_fail_count() { local n max + if sac_fail_count_increment_suppressed; then + write_log "SAC: пропуск sac-fail.count (shutdown/maintenance)" + return 1 + fi max="${SAC_FALLBACK_FAILURES:-5}" n="$(sac_read_fail_count)" n=$((n + 1)) diff --git a/ssh-monitor b/ssh-monitor index 02f031f..d9a8067 100644 --- a/ssh-monitor +++ b/ssh-monitor @@ -7,7 +7,7 @@ IFS=$'\n\t' # ============================================ CONFIG_FILE="/etc/ssh-monitor.conf" -SSH_MONITOR_VERSION="2.3.1-SAC" +SSH_MONITOR_VERSION="2.3.2-SAC" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$SCRIPT_DIR/ssh-monitor-perms.sh" ]; then @@ -2065,6 +2065,7 @@ send_shutdown_notification() { return 0 fi SHUTDOWN_NOTIFIED=1 + SAC_SUPPRESS_FAIL_COUNT=1 local timestamp timestamp=$(notification_date '+%d.%m.%Y %H:%M:%S') diff --git a/ssh-monitor-perms.sh b/ssh-monitor-perms.sh index e5d33a9..b963af2 100644 --- a/ssh-monitor-perms.sh +++ b/ssh-monitor-perms.sh @@ -215,6 +215,7 @@ ssh_monitor_validate_config_values() { esac for item in "${WHITELIST_IPS[@]:-}"; do + item="$(_ssh_monitor_config_trim "$item")" [ -z "$item" ] && continue if ! ssh_monitor_validate_ipv4 "$item"; then printf 'ERROR: WHITELIST_IPS: некорректный IPv4 %s\n' "$item" >&2 @@ -222,6 +223,7 @@ ssh_monitor_validate_config_values() { fi done for item in "${WHITELIST_SUBNETS[@]:-}"; do + item="$(_ssh_monitor_config_trim "$item")" [ -z "$item" ] && continue if ! ssh_monitor_validate_cidr_v4 "$item"; then printf 'ERROR: WHITELIST_SUBNETS: некорректный CIDR %s\n' "$item" >&2 diff --git a/ssh-monitor.conf.example b/ssh-monitor.conf.example index b2e83f6..2ae2b25 100644 --- a/ssh-monitor.conf.example +++ b/ssh-monitor.conf.example @@ -29,6 +29,7 @@ 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" +# Таймаут curl к SAC (health + ingest). При burst ingest на сервере — 60–90; см. docs/sac-ingest.ru.md SAC_TIMEOUT_SEC="45" SAC_SPOOL_FLUSH_MAX_FILES="50" SAC_SPOOL_MAX_AGE_HOURS="72" diff --git a/version.txt b/version.txt index 640db42..4579786 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.3.1-SAC +2.3.2-SAC