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>
This commit is contained in:
2026-07-08 09:44:24 +10:00
parent ec0857b8f2
commit 2048162057
11 changed files with 618 additions and 243 deletions
+113 -174
View File
@@ -1,174 +1,113 @@
# Автообновление ssh-monitor
Скрипт **`update_ssh_monitor.sh`** в корне репозитория обновляет установленный **`/usr/local/bin/ssh-monitor`** из git-репозитория и перезапускает systemd-сервис. Watchdog и таймеры **не изменяются**.
**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом deploy/обновлении в **существующий** конфиг **дописываются** отсутствующие SAC-переменные (`UseSAC`, `SAC_URL`, `SAC_API_KEY`, spool, timeout и т.д.) и выставляется **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**.
Установка самого монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md).
## Что делает скрипт
1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`).
2. Проверяет наличие **`ipset`**; при отсутствии пытается установить через пакетный менеджер (apt, yum, dnf, pacman, zypper). Без ipset скрипт завершается с ошибкой — новая версия монитора с **`ENABLE_IP_BAN=1`** требует ipset.
3. В **`UPDATE_DIR`** выполняет **`git clone`** или **`git pull origin main`** репозитория.
4. Ищет файл **`ssh-monitor`** в клоне:
- `$UPDATE_DIR/ssh-monitor/src/branch/main/ssh-monitor` (структура GitLab);
- `$UPDATE_DIR/ssh-monitor/ssh-monitor` (плоский репозиторий, как в этом проекте).
5. Сравнивает **SHA256** удалённого файла с **`LOCAL_SCRIPT_PATH`** (по умолчанию `/usr/local/bin/ssh-monitor`).
6. **Всегда** (независимо от шага 5) синхронизирует **`sac-client.sh`** в каталог рядом с монитором (по умолчанию **`/usr/local/bin/sac-client.sh`**) — нужен для **`UseSAC`** и **`--check-sac`**.
7. При отличии checksum **`ssh-monitor`** и/или **`sac-client.sh`**:
- резервная копия `ssh-monitor.backup.YYYYMMDD_HHMMSS` (только для основного скрипта);
- `cp` + `chmod +x` для `ssh-monitor`, `chmod 644` для `sac-client.sh`;
- **`systemctl restart ssh-monitor.service`** (если есть systemctl).
8. Пишет лог в **`LOG_FILE`** (по умолчанию `/var/log/update_script.log`).
```mermaid
flowchart LR
A[cron/timer] --> B[update_ssh_monitor.sh]
B --> C[git pull/clone]
C --> D[sync sac-client.sh]
C --> E{SHA256 ssh-monitor?}
D --> F{что-то обновилось?}
E -->|да| G[backup + cp ssh-monitor]
E -->|нет| F
G --> F
F -->|да| H[restart ssh-monitor.service]
F -->|нет| I[без изменений]
```
## Настройка перед первым запуском
Отредактируйте переменные в начале **`update_ssh_monitor.sh`**:
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона |
| `REPO_URL` | `https://git.kalinamall.ru/PapaTramp/ssh-monitor.git` | URL репозитория (на закрытом зеркале — URL своего Gitea) |
| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать бинарник |
| `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений |
| `DEBUG` | `0` | `1` — подробный DEBUG в лог |
Требования на сервере:
- **root** (или sudo) для `cp`, `systemctl`, установки ipset;
- **git** и сетевой доступ к репозиторию (SSH-ключ или HTTPS);
- ветка в скрипте жёстко **`main`** при другой ветке измените `git pull` в файле.
## Первичная установка (--deploy)
Один сценарий для нового сервера (клон git, `ssh-monitor`, `sac-client.sh`, пример конфига, systemd unit):
```bash
cd /path/to/ssh-monitor # или curl/clone репозитория
sudo ./first_deploy.sh
# то же самое:
sudo ./update_ssh_monitor.sh --deploy
```
Скрипт выводит в консоль, **куда что скопировано**, и список переменных для **`/etc/ssh-monitor.conf`**. Существующий конфиг **не перезаписывается**.
После установки отредактируйте конфиг и проверьте:
```bash
sudo nano /etc/ssh-monitor.conf
sudo /usr/local/bin/ssh-monitor --check-config
sudo /usr/local/bin/ssh-monitor --check-sac
sudo systemctl restart ssh-monitor.service
```
## Установка updater (для последующих обновлений)
```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 /opt/scripts/update_ssh_monitor.sh
sudo tail -20 /var/log/update_script.log
sudo systemctl status ssh-monitor
```
## Запуск по расписанию
### Cron (пример: каждый день в 04:00)
```bash
sudo crontab -e
```
Добавьте:
```
0 4 * * * /opt/scripts/update_ssh_monitor.sh
```
### systemd timer (альтернатива)
Создайте `/etc/systemd/system/ssh-monitor-update.service`:
```ini
[Unit]
Description=Update ssh-monitor from git
[Service]
Type=oneshot
ExecStart=/opt/scripts/update_ssh_monitor.sh
```
И `/etc/systemd/system/ssh-monitor-update.timer`:
```ini
[Unit]
Description=Daily ssh-monitor update
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-monitor-update.timer
```
## Откат после неудачного обновления
Резервные копии лежат рядом с установленным скриптом:
```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
```
## Лог и диагностика «мгновенного» завершения
Все шаги пишутся в **`/var/log/update_script.log`**. При обычном запуске те же строки дублируются в stdout (кроме **`--quiet`**).
В конце каждого прогона — блок **«Итог обновления»**: статус `ssh-monitor` / `sac-client` / git / перезапуск и **причина**, если файлы не копировались (checksum совпал, git pull failed, скрипт не найден в клоне).
```bash
sudo /opt/scripts/update_ssh_monitor.sh
sudo tail -40 /var/log/update_script.log
```
**Важно:** после `git pull` в клоне скрипт **сам обновляет** `/opt/scripts/update_ssh_monitor.sh` из репозитория и один раз перезапускает себя (если вызывали именно этот путь). Если в логе старые строки вроде `Checksums match` без блока «Итог обновления» — на сервере ещё старая копия updater; один раз:
```bash
sudo cp /opt/scripts/update/ssh-monitor/update_ssh_monitor.sh /opt/scripts/update_ssh_monitor.sh
sudo chmod 750 /opt/scripts/update_ssh_monitor.sh
sudo /opt/scripts/update_ssh_monitor.sh
```
## Ограничения
- Обновляется **только** файл **`ssh-monitor`**, не конфиг и не watchdog.
- При недоступном git **`git pull`** пишет предупреждение и продолжает с локальной копией.
- На **закрытом зеркале** в `update_ssh_monitor.sh` задайте **`REPO_URL`** своего Gitea; в публичном GitHub по умолчанию — `git.kalinamall.ru/PapaTramp/ssh-monitor`.
# Автообновление 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 pull`** репозитория.
4. Ищет **`ssh-monitor`** в клоне (GitLab layout или плоский репозиторий).
5. Сравнивает **SHA256** с **`LOCAL_SCRIPT_PATH`** (`/usr/local/bin/ssh-monitor`).
6. **Всегда** синхронизирует **`sac-client.sh`**, **`ssh-monitor-perms.sh`**, watchdog.
7. Дописывает ключи конфига; **`apply_runtime_security_hardening`**: `chmod 600` конфиг, `700` spool, `600` state/SAC-файлы, whitelist.
8. При изменении **`ssh-monitor`**, **`sac-client.sh`** или конфига — **`systemctl restart ssh-monitor.service`**.
9. Лог: **`/var/log/update_script.log`** (блок «Итог обновления», строка `security hardening`).
```mermaid
flowchart LR
A[SAC или cron] --> B[update_ssh_monitor.sh]
B --> C[git pull/clone]
C --> 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 |
+1 -1
View File
@@ -93,6 +93,6 @@ sudo ssh-monitor --check-config
## Каналы доставки
- **`NOTIFY_CHAIN`**: `telegram`, `email` — при каждом событии попытка **во все** каналы списка.
- **`BACKUP_WEBHOOK_URL`**: только если **все** каналы цепочки не доставили сообщение.
- **`BACKUP_WEBHOOK_URL`** *(deprecated, legacy)*: только если **все** каналы цепочки не доставили сообщение. В типовом деплое не используется; удаление в 2.3.x.
Режим **`--dry-run`**: уведомления не отправляются; в stderr печатается текст **уже с подписью сервера**.
+22 -32
View File
@@ -22,7 +22,8 @@
| Версия | Фазы | Суть |
|--------|------|------|
| **2.1.7-SAC** | Фаза 1 + выбранное из Фазы 4 | Права, `REPO_URL` обязателен, webhook, docs |
| **2.1.7-SAC** | Фаза 1 + выбранное из Фазы 4 | Права, `REPO_URL` обязателен, webhook, docs | ✅ реализовано |
| **2.1.8-SAC** | Дополнение к 1.1 | SAC-first update: hardening прав на каждом прогоне updater | ✅ реализовано |
| **2.2.0-SAC** | Фаза 2 | Manifest, pinned ref, без слепого `reset --hard` |
| **2.3.0-SAC** | Фаза 3 | Парсер конфига без `source` (minor breaking) |
@@ -34,9 +35,10 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре
### 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`: предупреждение, если существующие пути с ослабленными правами
- [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`
@@ -44,42 +46,30 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре
### 1.2 Проверка прав конфига при старте (C1, частично)
- [ ] Перед загрузкой конфига: если `/etc/ssh-monitor.conf` существует — проверить `root:root`, mode `600` или `400`
- [ ] **Поведение по умолчанию:** `WARN` в лог + stderr, работа продолжается (не ломать старые установки с `644`)
- [ ] Опционально в конфиге: `CONFIG_STRICT_PERMS=1`**exit 1** при нарушении
- [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) — **нужно утвердить завтра**
### 1.3 `BACKUP_WEBHOOK_URL` (H3) — **вариант A (soft-deprecate)**
**Что есть сейчас:** 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`
- [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** или позже, если подтвердим, что нигде не нужен
**Завтра решить:** 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 доменов — оператор сам выбирает зеркало
- [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 если есть
@@ -89,8 +79,8 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре
### 1.5 Документация threat model
- [ ] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub
- [ ] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно
- [x] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub
- [x] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно
---
@@ -220,8 +210,8 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре
| Пункт | Версия | Решение |
|-------|--------|---------|
| **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.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) |