diff --git a/docs/security-roadmap.ru.md b/docs/security-roadmap.ru.md new file mode 100644 index 0000000..50aea00 --- /dev/null +++ b/docs/security-roadmap.ru.md @@ -0,0 +1,265 @@ +# 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.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` в каждом релизе. + +--- + +## Фаза 1 — 2.1.7-SAC + +### 1.1 Права на state / spool (M4) + +- [ ] При создании каталогов и файлов состояния: `chown root:root`, каталоги `chmod 700`, файлы `chmod 600` +- [ ] Затронуть: `SAC_SPOOL_DIR`, `SAC_FAIL_COUNT_FILE`, heartbeat / last_* (где создаёт `ssh-monitor` / `sac-client.sh` / deploy) +- [ ] В `--check-config`: предупреждение, если существующие пути с ослабленными правами + +**Файлы:** `ssh-monitor`, `sac-client.sh`, при необходимости `update_ssh_monitor.sh` + +--- + +### 1.2 Проверка прав конфига при старте (C1, частично) + +- [ ] Перед загрузкой конфига: если `/etc/ssh-monitor.conf` существует — проверить `root:root`, mode `600` или `400` +- [ ] **Поведение по умолчанию:** `WARN` в лог + stderr, работа продолжается (не ломать старые установки с `644`) +- [ ] Опционально в конфиге: `CONFIG_STRICT_PERMS=1` → **exit 1** при нарушении + +**Решение агента:** strict выключен по умолчанию; в README рекомендовать `chmod 600` и позже `400`. + +--- + +### 1.3 `BACKUP_WEBHOOK_URL` (H3) — **нужно утвердить завтра** + +**Что есть сейчас:** fallback POST JSON `{"text":"..."}`, если **все** каналы `NOTIFY_CHAIN` не доставили сообщение; то же в watchdog при сбое Telegram. По умолчанию пусто. В проде, судя по обсуждению, **не используется**. + +| Вариант | Плюсы | Минусы | +|---------|-------|--------| +| **A. Soft-deprecate (рекомендация)** | Не ломает тех, у кого Slack webhook | Код остаётся | +| **B. Удалить в 2.1.7** | Меньше attack surface | Breaking, если кто-то использует | +| **C. Оставить как есть** | Без изменений | Риск H3 при компрометации конфига | + +**Рекомендация для 2.1.7 (вариант A):** + +- [ ] В `ssh-monitor.conf.example`: закомментировать / убрать из «активного» блока, комментарий `DEPRECATED: не используется в типовом деплое; будет удалён в 2.3.x` +- [ ] В README / `docs/notifications.ru.md`: пометить **legacy / необязательно** +- [ ] В `--check-config`: если задан — `WARN: BACKUP_WEBHOOK_URL deprecated` +- [ ] **Удаление кода** — отложить до **2.3.0** или позже, если подтвердим, что нигде не нужен + +**Завтра решить:** A / B / C. + +--- + +### 1.4 `REPO_URL` — только из env, без дефолта в коде (C3) — **утверждено** + +- [ ] Убрать захардкоженный default `https://git.kalinamall.ru/...` из `update_ssh_monitor.sh` +- [ ] При старте updater: если `REPO_URL` пуст — **exit 1**, сообщение в **stderr** и **`$LOG_FILE`** +- [ ] Текст ошибки: что задать (`export REPO_URL=...` или в systemd unit `Environment=REPO_URL=...`) +- [ ] SAC / cron / timer: документировать обязательную передачу `REPO_URL` (SAC уже может передавать при SSH-обновлении) +- [ ] **Не** делать allowlist доменов — оператор сам выбирает зеркало + +**Файлы:** `update_ssh_monitor.sh`, `docs/auto-update.ru.md`, пример unit/timer если есть + +**Заметка:** после 2.1.7 на всех хостах нужно явно прописать `REPO_URL` до следующего обновления. + +--- + +### 1.5 Документация threat model + +- [ ] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub +- [ ] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно + +--- + +## Фаза 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 без тегов | + +- [ ] Реализовать чтение `GIT_REF` / `GIT_VERIFY_MODE` в `update_ssh_monitor.sh` +- [ ] Дефолты для 2.2.0: `GIT_REF` = тег из `version.txt` или `main` (утвердить завтра); `GIT_VERIFY_MODE=tag` для prod-документации +- [ ] **`docs/auto-update.ru.md`**: отдельный раздел с примерами systemd и таблицей режимов (после реализации) + +--- + +### 2.2 Без слепого `reset --hard` (C2) — **утверждено** + +- [ ] При failed `git pull --ff-only`: **не** делать `reset --hard` автоматически +- [ ] Лог + stderr: «история разошлась, требуется ручное вмешательство или новый clone» +- [ ] Опционально: `GIT_ALLOW_RESET=1` только для ручного/CI (документировать риск) + +--- + +### 2.3 Release manifest (C2, C4) — **утверждено** + +- [ ] Файл в репо, например `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:..." + } +} +``` + +- [ ] Публиковать manifest при каждом релизе (в git, рядом с тегом) +- [ ] Updater: после checkout сверять SHA256 файлов из клона с manifest **до** копирования в `/usr/local/bin` +- [ ] Несовпадение → exit 1, ничего не перезаписывать +- [ ] Скрипт/цель в `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:** + +- [ ] **2.2.0:** не блокировать релиз на GPG; `GIT_VERIFY_MODE=tag` без `-v` +- [ ] **Backlog:** `GIT_GPG_VERIFY=1` + ключ в `/etc/ssh-monitor/trusted-release-key.asc`, если позже понадобится +- [ ] Документировать: при доверенном закрытом зеркале manifest + `GIT_REF=tag` — **достаточный минимум** + +--- + +### 2.5 Re-exec updater только после verify (C4) — **утверждено** + +- [ ] `UPDATER_REEXEC` / копирование `update_ssh_monitor.sh` в `/opt/scripts/` — только если manifest SHA256 совпал +- [ ] Иначе: лог, старая версия 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 на годы) +- Регулярные внешние аудиты + +--- + +## Чек-лист «перед стартом работы завтра» + +Утвердить галочками: + +- [ ] **Релиз 2.1.7** — scope: 1.1, 1.2 (warn), 1.4, 1.5, 4.4, 4.3 + **1.3 вариант A/B/C** +- [ ] **Релиз 2.2.0** — scope: 2.1–2.3, 2.5, 4.5, 4.7(M7); **2.4 отложить** +- [ ] **Релиз 2.3.0** — scope: 3.1–3.3, удаление webhook (если A) +- [ ] **Дефолт `GIT_REF`** в 2.2.0: тег из `version.txt` vs `main` +- [ ] **Миграция хостов 2.1.7:** рассылка примера `Environment=REPO_URL=...` для systemd / SAC +- [ ] **GPG:** отложить до явного запроса + +--- + +## Открытые вопросы на завтра + +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 версии.*