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.
This commit is contained in:
PTah
2026-07-08 11:46:50 +10:00
parent 275d305d62
commit 6df83f1838
13 changed files with 642 additions and 163 deletions
+4 -1
View File
@@ -1,10 +1,13 @@
# Версия берётся из ssh-monitor (SSH_MONITOR_VERSION). # Версия берётся из 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) VERSION := $(shell sed -n 's/^SSH_MONITOR_VERSION="\(.*\)".*/\1/p' ssh-monitor | head -n1)
DIST := ssh-monitor-$(VERSION).tar.gz DIST := ssh-monitor-$(VERSION).tar.gz
dist: $(DIST) dist: $(DIST)
manifest:
./scripts/build-release-manifest.sh
$(DIST): ssh-monitor $(DIST): ssh-monitor
git archive --format=tar.gz --prefix=ssh-monitor-$(VERSION)/ -o $(DIST) HEAD git archive --format=tar.gz --prefix=ssh-monitor-$(VERSION)/ -o $(DIST) HEAD
+2 -2
View File
@@ -1,6 +1,6 @@
# ssh-monitor # ssh-monitor
**Версия:** `2.1.9-SAC` **Версия:** `2.2.1-SAC`
Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, бан IP (ipset), ежедневный отчёт, heartbeat. Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, бан IP (ipset), ежедневный отчёт, heartbeat.
@@ -14,7 +14,7 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email,
|------|------------| |------|------------|
| **`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`** | Основной цикл: разбор журналов (**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`. | | **`ssh-monitor-watchdog`** | Отдельный скрипт (обычно по **systemd timer**): проверяет, что сервис монитора активен и файл heartbeat не старше порога; при сбое выполняет **`systemctl restart`** и шлёт уведомление в Telegram или в **`BACKUP_WEBHOOK_URL`**. Читает те же `TELEGRAM_*` / webhook из `/etc/ssh-monitor.conf`. |
| `update_ssh_monitor.sh` | Опциональное автообновление: `git pull` / `clone` (дефолт `REPO_URL=git.kalinamall.ru`, override через env), сравнение SHA256, **`sac-client.sh`**, перезапуск `ssh-monitor.service`. См. [docs/auto-update.ru.md](docs/auto-update.ru.md). | | `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). **Системные пакеты и зависимости** (что поставить на сервер до запуска): пошагово в [docs/install-prerequisites.ru.md](docs/install-prerequisites.ru.md).
+174 -113
View File
@@ -1,113 +1,174 @@
# Автообновление ssh-monitor # Автообновление ssh-monitor
Скрипт **`update_ssh_monitor.sh`** обновляет **`/usr/local/bin/ssh-monitor`** из git и перезапускает systemd-сервис.
Скрипт **`update_ssh_monitor.sh`** обновляет **`/usr/local/bin/ssh-monitor`** из git и перезапускает systemd-сервис. ## Основной путь: Security Alert Center
На продакшен-хостах обновление идёт **через SAC**, без cron:
## Основной путь: Security Alert Center 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**, без cron: Участие админа на сервере **не требуется** (кроме первичной настройки `SAC_URL` / `SAC_API_KEY` в конфиге).
При **первой установке** (нет updater на хосте) SAC выполняет bootstrap с **`--deploy`**: clone, ipset при необходимости, systemd unit, полный hardening.
1. Админ нажимает **«Обновить ssh-monitor (SSH)»** в карточке хоста. **Cron / systemd timer** — опциональная альтернатива с тем же скриптом и обязательным `REPO_URL` в окружении.
2. SAC по SSH запускает `/opt/scripts/update_ssh_monitor.sh` с `REPO_URL` / `GIT_BRANCH` из **Настройки → Обновления агентов**. Watchdog и таймеры watchdog **не изменяются** при обычном update (кроме синхронизации скрипта watchdog из git).
3. Updater сам выполняет `git pull`, синхронизирует `sac-client.sh` и watchdog, дописывает отсутствующие ключи в **`/etc/ssh-monitor.conf`**, **выставляет `chmod`/`chown`** на конфиг, SAC-spool, счётчики и state-файлы, перезапускает **`ssh-monitor.service`**. **Конфиг `/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).
Участие админа на сервере **не требуется** (кроме первичной настройки `SAC_URL` / `SAC_API_KEY` в конфиге). ## Что делает скрипт
1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`).
2. При **`--deploy`** / bootstrap SAC проверяет **`ipset`** и при отсутствии пытается установить через пакетный менеджер. Обычное обновление **не** вызывает `apt-get`.
При **первой установке** (нет updater на хосте) SAC выполняет bootstrap с **`--deploy`**: clone, ipset при необходимости, systemd unit, полный hardening. 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`).
**Cron / systemd timer** — опциональная альтернатива с тем же скриптом и обязательным `REPO_URL` в окружении. 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`).
Watchdog и таймеры watchdog **не изменяются** при обычном update (кроме синхронизации скрипта watchdog из git).
```mermaid
flowchart LR
A[SAC или cron] --> B[update_ssh_monitor.sh]
**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом update в **существующий** конфиг **дописываются** отсутствующие SAC-переменные и ключи безопасности; **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**. B --> C[git fetch/checkout]
C --> M{manifest OK?}
M -->|нет| X[exit 1]
M -->|да/skip| D[sync sac-client + perms lib]
Установка монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md). 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[без перезапуска]
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 или плоский репозиторий). | `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона |
| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron |
5. Сравнивает **SHA256** с **`LOCAL_SCRIPT_PATH`** (`/usr/local/bin/ssh-monitor`). | `GIT_BRANCH` | `main` | Ветка для clone/pull, если **`GIT_REF`** не задан |
| `GIT_REF` | *(пусто)* | Тег (`2.2.0-SAC`), ветка или commit SHA для checkout |
6. **Всегда** синхронизирует **`sac-client.sh`**, **`ssh-monitor-perms.sh`**, watchdog. | `GIT_VERIFY_MODE` | `off` | `off` \| `tag` \| `commit` — см. раздел ниже |
| `GIT_ALLOW_RESET` | `0` | `1` = разрешить `git reset --hard` при расхождении истории (риск) |
7. Дописывает ключи конфига; **`apply_runtime_security_hardening`**: `chmod 600` конфиг, `700` spool, `600` state/SAC-файлы, whitelist. | `RELEASE_MANIFEST_STRICT` | `0` | `1` = exit 1, если manifest для версии не найден |
| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать монитор |
8. При изменении **`ssh-monitor`**, **`sac-client.sh`** или конфига — **`systemctl restart ssh-monitor.service`**. | `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений |
| `UPDATE_VIA_SAC` | *(опционально)* | SAC выставляет `1`; при отсутствии бинарника включается `--deploy` |
9. Лог: **`/var/log/update_script.log`** (блок «Итог обновления», строка `security hardening`).
Требования: **root**, **git**, **python3** (manifest verify), доступ к репозиторию.
## Pinned ref и manifest (2.2.0+)
```mermaid
Updater читает **`GIT_REF`**, **`GIT_VERIFY_MODE`**, **`GIT_ALLOW_RESET`** из env или `/etc/ssh-monitor.conf`.
flowchart LR
### Режимы `GIT_VERIFY_MODE`
A[SAC или cron] --> B[update_ssh_monitor.sh]
| Режим | Поведение | Когда |
B --> C[git pull/clone] |-------|-----------|-------|
| **`off`** | После fetch принять ref как есть | Тест, legacy; дефолт в скрипте |
C --> D[sync sac-client + perms lib] | **`tag`** | `GIT_REF` должен существовать как тег | **Рекомендуется для прода** |
| **`commit`** | `GIT_REF` = полный SHA commit | Жёсткий pin без тегов |
C --> E{SHA256 ssh-monitor?}
Если **`GIT_REF`** пуст, используется **`GIT_BRANCH`** (`git pull --ff-only`). При невозможности fast-forward updater **не** делает `reset --hard` (только с **`GIT_ALLOW_RESET=1`**).
D --> F[harden config/spool/state]
### Release manifest
E -->|да| G[backup + cp ssh-monitor]
В репозитории: **`release/manifest-VERSION.json`** — SHA256 ключевых файлов и `git_commit`. Updater сверяет manifest **после** git sync и **до** копирования бинарников.
E -->|нет| F
- Файл найден и hash не совпал → **exit 1**, ничего не перезаписывается.
G --> F - Файл не найден → WARN, update продолжается (или exit 1 при **`RELEASE_MANIFEST_STRICT=1`**).
- Копирование **`update_ssh_monitor.sh`** в `/opt/scripts/` и re-exec — только если manifest для версии был и прошёл verify.
F --> H{что-то изменилось?}
Генерация при релизе (после **финального** коммита кода, чтобы `git_commit` в manifest совпал с HEAD):
H -->|да| I[restart ssh-monitor.service]
```bash
H -->|нет| J[без перезапуска] 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
| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона | ```
| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron | **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).
+21 -20
View File
@@ -25,12 +25,13 @@
| **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.1.8-SAC** | Дополнение к 1.1 | SAC-first update: hardening прав на каждом прогоне updater | ✅ реализовано |
| **2.1.9-SAC** | Дополнение к 1.x | Watchdog: `🖥️ Сервер:` в Telegram (как у агента) | ✅ реализовано | | **2.1.9-SAC** | Дополнение к 1.x | Watchdog: `🖥️ Сервер:` в Telegram (как у агента) | ✅ реализовано |
| **2.2.0-SAC** | **Фаза 2** | Manifest, pinned ref, без слепого `reset --hard` | **следующий релиз** | | **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) | | **2.3.0-SAC** | Фаза 3 | Парсер конфига без `source` (minor breaking) |
Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом релизе. Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом релизе.
**Текущий статус (2026-07-08):** фаза 1 закрыта (2.1.72.1.9). **Дальше — фаза 2 → релиз 2.2.0-SAC.** **Текущий статус (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). 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).
--- ---
@@ -107,23 +108,23 @@ Backlog по ingest при массовом update (SAC + ops): [ingest-mass-upd
| **`tag`** | `GIT_REF` должен быть **аннотированным тегом**; опционально `git tag -v` (см. 2.4) | **Рекомендуется для прода** | | **`tag`** | `GIT_REF` должен быть **аннотированным тегом**; опционально `git tag -v` (см. 2.4) | **Рекомендуется для прода** |
| **`commit`** | `GIT_REF` = полный SHA; сверка с manifest | Жёсткий pin без тегов | | **`commit`** | `GIT_REF` = полный SHA; сверка с manifest | Жёсткий pin без тегов |
- [ ] Реализовать чтение `GIT_REF` / `GIT_VERIFY_MODE` в `update_ssh_monitor.sh` - [x] Реализовать чтение `GIT_REF` / `GIT_VERIFY_MODE` в `update_ssh_monitor.sh`
- [ ] Дефолты для 2.2.0: `GIT_REF` = тег из `version.txt` или `main` (утвердить завтра); `GIT_VERIFY_MODE=tag` для prod-документации - [x] Дефолты для 2.2.0: `GIT_REF` пуст → `GIT_BRANCH`; `GIT_VERIFY_MODE=off` в скрипте, **`tag`** в prod-документации
- [ ] **`docs/auto-update.ru.md`**: отдельный раздел с примерами systemd и таблицей режимов (после реализации) - [x] **`docs/auto-update.ru.md`**: отдельный раздел с примерами systemd и таблицей режимов
--- ---
### 2.2 Без слепого `reset --hard` (C2) — **утверждено** ### 2.2 Без слепого `reset --hard` (C2) — **утверждено**
- [ ] При failed `git pull --ff-only`: **не** делать `reset --hard` автоматически - [x] При failed `git pull --ff-only`: **не** делать `reset --hard` автоматически
- [ ] Лог + stderr: «история разошлась, требуется ручное вмешательство или новый clone» - [x] Лог + stderr: «история разошлась, требуется ручное вмешательство или новый clone»
- [ ] Опционально: `GIT_ALLOW_RESET=1` только для ручного/CI (документировать риск) - [x] Опционально: `GIT_ALLOW_RESET=1` только для ручного/CI (документировать риск)
--- ---
### 2.3 Release manifest (C2, C4) — **утверждено** ### 2.3 Release manifest (C2, C4) — **утверждено**
- [ ] Файл в репо, например `release/manifest-2.2.0-SAC.json`: - [x] Файл в репо, например `release/manifest-2.2.0-SAC.json`:
```json ```json
{ {
@@ -138,10 +139,10 @@ Backlog по ingest при массовом update (SAC + ops): [ingest-mass-upd
} }
``` ```
- [ ] Публиковать manifest при каждом релизе (в git, рядом с тегом) - [x] Публиковать manifest при каждом релизе (в git, рядом с тегом)
- [ ] Updater: после checkout сверять SHA256 файлов из клона с manifest **до** копирования в `/usr/local/bin` - [x] Updater: после checkout сверять SHA256 файлов из клона с manifest **до** копирования в `/usr/local/bin`
- [ ] Несовпадение → exit 1, ничего не перезаписывать - [x] Несовпадение → exit 1, ничего не перезаписывать
- [ ] Скрипт/цель в `Makefile` или `scripts/build-release-manifest.sh` для генерации - [x] Скрипт/цель в `Makefile` или `scripts/build-release-manifest.sh` для генерации
**Связь с 2.4:** manifest из **того же** `REPO_URL` защищает от подмены файлов в clone и от «не того» коммита; не защищает от полной компрометации git-сервера (для этого GPG). **Связь с 2.4:** manifest из **того же** `REPO_URL` защищает от подмены файлов в clone и от «не того» коммита; не защищает от полной компрометации git-сервера (для этого GPG).
@@ -159,16 +160,16 @@ Backlog по ingest при массовом update (SAC + ops): [ingest-mass-upd
**Решение для roadmap:** **Решение для roadmap:**
- [ ] **2.2.0:** не блокировать релиз на GPG; `GIT_VERIFY_MODE=tag` без `-v` - [x] **2.2.0:** не блокировать релиз на GPG; `GIT_VERIFY_MODE=tag` без `-v`
- [ ] **Backlog:** `GIT_GPG_VERIFY=1` + ключ в `/etc/ssh-monitor/trusted-release-key.asc`, если позже понадобится - [ ] **Backlog:** `GIT_GPG_VERIFY=1` + ключ в `/etc/ssh-monitor/trusted-release-key.asc`, если позже понадобится
- [ ] Документировать: при доверенном закрытом зеркале manifest + `GIT_REF=tag`**достаточный минимум** - [x] Документировать: при доверенном закрытом зеркале manifest + `GIT_REF=tag`**достаточный минимум**
--- ---
### 2.5 Re-exec updater только после verify (C4) — **утверждено** ### 2.5 Re-exec updater только после verify (C4) — **утверждено**
- [ ] `UPDATER_REEXEC` / копирование `update_ssh_monitor.sh` в `/opt/scripts/` — только если manifest SHA256 совпал - [x] `UPDATER_REEXEC` / копирование `update_ssh_monitor.sh` в `/opt/scripts/` — только если manifest SHA256 совпал
- [ ] Иначе: лог, старая версия updater остаётся, exit 1 - [x] Иначе: лог, старая версия updater остаётся, exit 1
--- ---
@@ -216,12 +217,12 @@ Backlog по ingest при массовом update (SAC + ops): [ingest-mass-upd
|-------|--------|---------| |-------|--------|---------|
| **4.4** `ensure_ipset_installed` не на каждый update | 2.1.7 | **Делаем:** только `--deploy` / `first_deploy.sh`; обычный update не вызывает `apt-get` | ✅ | | **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.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.5** JSON healthcheck через `json.dumps` | 2.2.0 | **Делаем:** мелкий fix L3 | ✅ |
| **4.1** Секреты не через environ в Python | — | **Не делаем** (мало выигрыша при root) | | **4.1** Секреты не через environ в Python | — | **Не делаем** (мало выигрыша при root) |
| **4.2** Telegram token в URL | — | **Не делаем** (ограничение Bot API) | | **4.2** Telegram token в URL | — | **Не делаем** (ограничение Bot API) |
| **4.6** systemd hardening (non-root) | — | **Не делаем** (ipset/iptables требуют root) | | **4.6** systemd hardening (non-root) | — | **Не делаем** (ipset/iptables требуют root) |
| **M6** Ротация логов | — | Уже есть `contrib/logrotate.d/` — только упоминание в README | | **M6** Ротация логов | — | Уже есть `contrib/logrotate.d/` — только упоминание в README |
| **M7** `set_config_kv` sed | 2.2.0 | **Делаем:** экранирование `/` и `"` в `val` при merge конфига | | **M7** `set_config_kv` sed | 2.2.0 | **Делаем:** экранирование `/` и `"` в `val` при merge конфига | ✅ |
--- ---
@@ -237,7 +238,7 @@ Backlog по ingest при массовом update (SAC + ops): [ingest-mass-upd
## Чек-лист «перед стартом работы завтра» ## Чек-лист «перед стартом работы завтра»
- [x] **Релиз 2.1.72.1.9** — фаза 1 + SAC-first + watchdog server label - [x] **Релиз 2.1.72.1.9** — фаза 1 + SAC-first + watchdog server label
- [ ] **Релиз 2.2.0** — scope: 2.12.3, 2.5, 4.5, 4.7(M7); **2.4 GPG отложить** - [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) - [ ] **Релиз 2.3.0** — scope: 3.13.3, удаление webhook (если A)
- [ ] **Дефолт `GIT_REF`** в 2.2.0: тег из `version.txt` vs `main` - [ ] **Дефолт `GIT_REF`** в 2.2.0: тег из `version.txt` vs `main`
- [x] **Миграция хостов:** SAC-first update, `known_hosts`, `REPO_URL` через SAC - [x] **Миграция хостов:** SAC-first update, `known_hosts`, `REPO_URL` через SAC
+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"
}
}
+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
+18 -5
View File
@@ -7,7 +7,7 @@ IFS=$'\n\t'
# ============================================ # ============================================
CONFIG_FILE="/etc/ssh-monitor.conf" CONFIG_FILE="/etc/ssh-monitor.conf"
SSH_MONITOR_VERSION="2.1.9-SAC" SSH_MONITOR_VERSION="2.2.1-SAC"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/ssh-monitor-perms.sh" ]; then if [ -f "$SCRIPT_DIR/ssh-monitor-perms.sh" ]; then
@@ -2018,10 +2018,15 @@ write_health_and_metrics() {
ts=$(date '+%s') ts=$(date '+%s')
if [ -n "${HEALTHCHECK_STATUS_FILE:-}" ]; then if [ -n "${HEALTHCHECK_STATUS_FILE:-}" ]; then
ssh_monitor_secure_dir "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || mkdir -p "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || true ssh_monitor_secure_dir "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || mkdir -p "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || true
printf '{"ts":%s,"hostname":"%s","dry_run":%s}\n' "$ts" "$(hostname | sed 's/"/\\"/g')" "$([[ "${DRY_RUN:-0}" = "1" ]] && echo true || echo false)" >"${HEALTHCHECK_STATUS_FILE}.tmp" if command -v python3 >/dev/null 2>&1; then
ssh_monitor_secure_file "${HEALTHCHECK_STATUS_FILE}.tmp" 2>/dev/null || true python3 -c 'import json,sys; print(json.dumps({"ts": int(sys.argv[1]), "hostname": sys.argv[2], "dry_run": sys.argv[3] == "1"}))' \
mv "${HEALTHCHECK_STATUS_FILE}.tmp" "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true "$ts" "$(hostname)" "${DRY_RUN:-0}" >"${HEALTHCHECK_STATUS_FILE}.tmp"
ssh_monitor_secure_file "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true ssh_monitor_secure_file "${HEALTHCHECK_STATUS_FILE}.tmp" 2>/dev/null || true
mv "${HEALTHCHECK_STATUS_FILE}.tmp" "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true
ssh_monitor_secure_file "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true
else
write_log "WARN: python3 недоступен — HEALTHCHECK_STATUS_FILE пропущен"
fi
fi fi
if [ -n "${PROMETHEUS_TEXTFILE_DIR:-}" ]; then if [ -n "${PROMETHEUS_TEXTFILE_DIR:-}" ]; then
mkdir -p "$PROMETHEUS_TEXTFILE_DIR" 2>/dev/null || true mkdir -p "$PROMETHEUS_TEXTFILE_DIR" 2>/dev/null || true
@@ -2040,6 +2045,10 @@ write_health_and_metrics() {
# ============================================ # ============================================
send_startup_notification() { send_startup_notification() {
if ssh_monitor_update_in_progress 2>/dev/null; then
write_log "lifecycle startup: пропуск (идёт agent-update)"
return 0
fi
local timestamp local timestamp
timestamp=$(notification_date '+%d.%m.%Y %H:%M:%S') timestamp=$(notification_date '+%d.%m.%Y %H:%M:%S')
local whitelist_count=$((${#WHITELIST_IPS[@]} + ${#WHITELIST_SUBNETS[@]})) local whitelist_count=$((${#WHITELIST_IPS[@]} + ${#WHITELIST_SUBNETS[@]}))
@@ -2067,6 +2076,10 @@ send_startup_notification() {
send_shutdown_notification() { send_shutdown_notification() {
[ "${SHUTDOWN_NOTIFIED:-0}" = "1" ] && return 0 [ "${SHUTDOWN_NOTIFIED:-0}" = "1" ] && return 0
if ssh_monitor_update_in_progress 2>/dev/null; then
write_log "lifecycle shutdown: пропуск (идёт agent-update)"
return 0
fi
SHUTDOWN_NOTIFIED=1 SHUTDOWN_NOTIFIED=1
local timestamp local timestamp
+34
View File
@@ -171,3 +171,37 @@ ssh_monitor_message_ensure_server_line() {
printf '%s\n🖥️ Сервер: %s\n%s' "$first" "$(ssh_monitor_server_display_name_with_ip)" "$rest" printf '%s\n🖥️ Сервер: %s\n%s' "$first" "$(ssh_monitor_server_display_name_with_ip)" "$rest"
fi 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
}
+5
View File
@@ -142,6 +142,11 @@ main() {
mkdir -p "$(dirname "$WATCHDOG_LOG_FILE")" mkdir -p "$(dirname "$WATCHDOG_LOG_FILE")"
touch "$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="" local reason=""
if ! is_service_active; then if ! is_service_active; then
reason="systemd reported inactive service" reason="systemd reported inactive service"
+8
View File
@@ -108,6 +108,14 @@ PROMETHEUS_TEXTFILE_DIR=""
# JSON-снимок состояния на каждой итерации (timestamp, hostname, dry_run). Пусто — отключено. # JSON-снимок состояния на каждой итерации (timestamp, hostname, dry_run). Пусто — отключено.
HEALTHCHECK_STATUS_FILE="" HEALTHCHECK_STATUS_FILE=""
# --- Автообновление (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). # Порог устаревания heartbeat для watchdog (сек): интервал + 1 ч запас (240 мин → 18000).
WATCHDOG_MAX_HEARTBEAT_AGE="18000" WATCHDOG_MAX_HEARTBEAT_AGE="18000"
WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log" WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log"
+271 -21
View File
@@ -5,6 +5,11 @@ UPDATE_DIR="/opt/scripts/update"
SCRIPT_NAME="ssh-monitor" SCRIPT_NAME="ssh-monitor"
REPO_URL="${REPO_URL:-}" REPO_URL="${REPO_URL:-}"
GIT_BRANCH="${GIT_BRANCH:-main}" GIT_BRANCH="${GIT_BRANCH:-main}"
GIT_REF="${GIT_REF:-}"
GIT_VERIFY_MODE="${GIT_VERIFY_MODE:-off}"
GIT_ALLOW_RESET="${GIT_ALLOW_RESET:-0}"
MANIFEST_VERIFIED=0
MANIFEST_STRICT=0
LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor" LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor"
LOG_FILE="/var/log/update_script.log" LOG_FILE="/var/log/update_script.log"
@@ -24,6 +29,7 @@ SUMMARY_SERVICE_RESTART="—"
SUMMARY_UPDATER="—" SUMMARY_UPDATER="—"
SUMMARY_WATCHDOG="—" SUMMARY_WATCHDOG="—"
SUMMARY_HARDENING="—" SUMMARY_HARDENING="—"
SUMMARY_MANIFEST="—"
CONFIG_FILE="/etc/ssh-monitor.conf" CONFIG_FILE="/etc/ssh-monitor.conf"
DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh" DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh"
@@ -401,11 +407,12 @@ maybe_enable_deploy_for_missing_install() {
} }
set_config_kv() { set_config_kv() {
local key="$1" val="$2" file="$3" local key="$1" val="$2" file="$3" esc
esc=$(printf '%s' "$val" | sed -e 's/[\\&/]/\\&/g' -e 's/"/\\"/g')
if grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null; then if grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null; then
sed -i -E "s/^[[:space:]]*${key}=.*/${key}=\"${val}\"/" "$file" sed -i -E "s/^[[:space:]]*${key}=.*/${key}=\"${esc}\"/" "$file"
else else
printf '%s="%s"\n' "$key" "$val" >>"$file" printf '%s="%s"\n' "$key" "$esc" >>"$file"
fi fi
} }
@@ -619,44 +626,238 @@ resolve_git_remote_name() {
printf '%s' "$name" printf '%s' "$name"
} }
# fetch + ff-only pull; при расхождении истории (force-push) — reset --hard. load_git_verify_settings() {
local v
if [ -f "$CONFIG_FILE" ]; then
if [ -z "${GIT_REF:-}" ]; then
v=$(config_conf_value GIT_REF "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_REF="$v"
fi
if [ -z "${GIT_VERIFY_MODE:-}" ] || [ "$GIT_VERIFY_MODE" = "off" ]; then
v=$(config_conf_value GIT_VERIFY_MODE "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_VERIFY_MODE="$v"
fi
if [ "${GIT_ALLOW_RESET:-0}" = "0" ]; then
v=$(config_conf_value GIT_ALLOW_RESET "$CONFIG_FILE" 2>/dev/null || true)
[ -n "$v" ] && GIT_ALLOW_RESET="$v"
fi
fi
GIT_VERIFY_MODE="$(printf '%s' "${GIT_VERIFY_MODE:-off}" | tr '[:upper:]' '[:lower:]')"
case "$GIT_VERIFY_MODE" in
off | tag | commit) ;;
*)
log_action "WARN: GIT_VERIFY_MODE=$GIT_VERIFY_MODE неизвестен — off"
GIT_VERIFY_MODE=off
;;
esac
case "${GIT_ALLOW_RESET:-0}" in
1 | yes | true | on) GIT_ALLOW_RESET=1 ;;
*) GIT_ALLOW_RESET=0 ;;
esac
case "${RELEASE_MANIFEST_STRICT:-0}" in
1 | yes | true | on) MANIFEST_STRICT=1 ;;
*) MANIFEST_STRICT=0 ;;
esac
}
verify_git_ref() {
local repo="$1" ref="$2" mode="$3"
local obj_type full
[ "$mode" = "off" ] && return 0
[ -n "$ref" ] || {
log_action "ERROR: GIT_VERIFY_MODE=$mode требует GIT_REF"
return 1
}
case "$mode" in
tag)
if ! git -C "$repo" rev-parse "refs/tags/$ref" >/dev/null 2>&1; then
log_action "ERROR: GIT_REF=$ref не найден как тег в клоне"
return 1
fi
obj_type=$(git -C "$repo" cat-file -t "refs/tags/$ref" 2>/dev/null || true)
if [ "$obj_type" != "tag" ] && [ "$obj_type" != "commit" ]; then
log_action "ERROR: GIT_REF=$ref: неожиданный тип объекта ($obj_type)"
return 1
fi
log_action "git verify: тег $ref OK (type=$obj_type)"
;;
commit)
full=$(git -C "$repo" rev-parse "$ref^{commit}" 2>/dev/null || true)
if [ -z "$full" ] || [ "${#full}" -ne 40 ]; then
log_action "ERROR: GIT_REF=$ref не является полным commit SHA"
return 1
fi
log_action "git verify: commit ${full:0:12}… OK"
;;
esac
return 0
}
find_release_manifest_path() {
local repo="$1" version="$2"
local p="$repo/release/manifest-${version}.json"
if [ -f "$p" ]; then
printf '%s\n' "$p"
return 0
fi
return 1
}
read_repo_release_version() {
local repo="$1" ver
ver=$(read_ssh_monitor_version "$repo/ssh-monitor")
if [ -z "$ver" ] && [ -f "$repo/version.txt" ]; then
ver=$(tr -d '\r\n' <"$repo/version.txt")
fi
printf '%s' "$ver"
}
verify_release_manifest() {
local repo="$1" version manifest_path rc
MANIFEST_VERIFIED=0
version=$(read_repo_release_version "$repo")
if [ -z "$version" ]; then
SUMMARY_MANIFEST="версия не определена"
log_action "WARN: не удалось определить версию релиза для manifest"
[ "$MANIFEST_STRICT" -eq 1 ] && return 1
return 0
fi
manifest_path=$(find_release_manifest_path "$repo" "$version" 2>/dev/null || true)
if [ -z "$manifest_path" ]; then
SUMMARY_MANIFEST="manifest-${version}.json не найден (skip)"
log_action "WARN: release/manifest-${version}.json не найден — проверка SHA пропущена"
if [ "$MANIFEST_STRICT" -eq 1 ]; then
log_action "ERROR: RELEASE_MANIFEST_STRICT=1 — manifest обязателен"
return 1
fi
return 0
fi
log_action "manifest: проверка $manifest_path"
if ! MANIFEST_PATH="$manifest_path" REPO_DIR="$repo" python3 <<'PY' >>"$LOG_FILE" 2>&1
import hashlib
import json
import os
import subprocess
import sys
manifest_path = os.environ["MANIFEST_PATH"]
repo = os.environ["REPO_DIR"]
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
expected_commit = (manifest.get("git_commit") or "").strip()
if expected_commit:
actual = subprocess.check_output(["git", "-C", repo, "rev-parse", "HEAD"], text=True).strip()
if actual != expected_commit:
print(f"commit mismatch: HEAD={actual[:12]} manifest={expected_commit[:12]}", file=sys.stderr)
sys.exit(2)
files = manifest.get("files") or {}
for rel, spec in files.items():
path = os.path.join(repo, rel)
if not os.path.isfile(path):
print(f"missing file: {rel}", file=sys.stderr)
sys.exit(3)
want = str(spec).removeprefix("sha256:")
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
got = h.hexdigest()
if got != want:
print(f"sha256 mismatch: {rel}", file=sys.stderr)
sys.exit(4)
print("OK")
PY
then
SUMMARY_MANIFEST="ОШИБКА verify"
log_action "ERROR: manifest verify failed (см. $LOG_FILE)"
return 1
fi
MANIFEST_VERIFIED=1
SUMMARY_MANIFEST="OK ($version)"
log_action "manifest: verify OK ($version)"
return 0
}
# fetch + checkout GIT_REF или fast-forward по GIT_BRANCH; без reset --hard (кроме GIT_ALLOW_RESET=1).
sync_git_repository() { sync_git_repository() {
local repo="$1" branch="${2:-main}" local repo="$1" branch="${2:-main}"
local remote upstream git_before git_after local remote upstream git_before git_after ref="${GIT_REF:-}"
remote="$(resolve_git_remote_name "$repo")" remote="$(resolve_git_remote_name "$repo")"
upstream="${remote}/${branch}" upstream="${remote}/${branch}"
if [ -f "$repo/.git/MERGE_HEAD" ]; then if [ -f "$repo/.git/MERGE_HEAD" ]; then
log_action "git: незавершённый merge — abort" log_action "git: незавершённый merge — abort"
git -C "$repo" merge --abort >> "$LOG_FILE" 2>&1 || true git -C "$repo" merge --abort >>"$LOG_FILE" 2>&1 || true
fi fi
git_before="$(get_git_short_rev "$repo")" git_before="$(get_git_short_rev "$repo")"
if [ -n "$ref" ]; then
log_action "git: fetch $remote (ref=$ref, verify=$GIT_VERIFY_MODE) → $REPO_URL"
if ! git -C "$repo" fetch --prune "$remote" \
"+refs/heads/*:refs/remotes/${remote}/*" \
"+refs/tags/*:refs/tags/*" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="fetch FAILED ($git_before)"
log_action "ERROR: git fetch failed (см. $LOG_FILE)"
return 1
fi
if ! verify_git_ref "$repo" "$ref" "$GIT_VERIFY_MODE"; then
SUMMARY_GIT="verify FAILED ($ref)"
return 1
fi
if ! git -C "$repo" checkout -f "$ref" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="checkout FAILED ($ref)"
log_action "ERROR: git checkout $ref failed (см. $LOG_FILE)"
return 1
fi
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="checkout $ref ok, $git_before$git_after"
log_action "git: checkout $ref OK ($git_before$git_after)"
return 0
fi
log_action "git: fetch --prune $remote $branch (было $git_before) → $REPO_URL" log_action "git: fetch --prune $remote $branch (было $git_before) → $REPO_URL"
if ! git -C "$repo" fetch --prune "$remote" "$branch" >> "$LOG_FILE" 2>&1; then if ! git -C "$repo" fetch --prune "$remote" "$branch" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="fetch FAILED ($git_before)" SUMMARY_GIT="fetch FAILED ($git_before)"
log_action "ERROR: git fetch failed (см. $LOG_FILE)" log_action "ERROR: git fetch failed (см. $LOG_FILE)"
return 1 return 1
fi fi
if git -C "$repo" pull --ff-only "$remote" "$branch" >> "$LOG_FILE" 2>&1; then if git -C "$repo" pull --ff-only "$remote" "$branch" >>"$LOG_FILE" 2>&1; then
git_after="$(get_git_short_rev "$repo")" git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="pull ok, $git_before$git_after" SUMMARY_GIT="pull ok, $git_before$git_after"
log_action "git: fast-forward OK ($git_before$git_after)" log_action "git: fast-forward OK ($git_before$git_after)"
return 0 return 0
fi fi
log_action "WARN: fast-forward невозможен (часто после force-push) — reset --hard $upstream" if [ "$GIT_ALLOW_RESET" -eq 1 ]; then
if ! git -C "$repo" reset --hard "$upstream" >> "$LOG_FILE" 2>&1; then log_action "WARN: fast-forward невозможен — GIT_ALLOW_RESET=1, reset --hard $upstream"
SUMMARY_GIT="reset FAILED ($git_before)" if ! git -C "$repo" reset --hard "$upstream" >>"$LOG_FILE" 2>&1; then
log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)" SUMMARY_GIT="reset FAILED ($git_before)"
return 1 log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)"
return 1
fi
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="reset ok, $git_before$git_after"
log_action "git reset --hard: $git_before$git_after"
return 0
fi fi
git_after="$(get_git_short_rev "$repo")"
SUMMARY_GIT="reset ok, $git_before$git_after" SUMMARY_GIT="diverged ($git_before)"
log_action "git reset --hard: $git_before$git_after" log_action "ERROR: git: история разошлась (ff-only невозможен). Ручной clone или GIT_ALLOW_RESET=1"
return 0 echo "ERROR: git pull --ff-only failed; история разошлась. Исправьте клон в $repo или задайте GIT_ALLOW_RESET=1 (риск)." >&2
return 1
} }
read_ssh_monitor_version() { read_ssh_monitor_version() {
@@ -705,6 +906,7 @@ log_update_summary() {
log_action "updater ($DEPLOY_INSTALL_PATH): $SUMMARY_UPDATER" log_action "updater ($DEPLOY_INSTALL_PATH): $SUMMARY_UPDATER"
log_action "watchdog: $SUMMARY_WATCHDOG" log_action "watchdog: $SUMMARY_WATCHDOG"
log_action "security hardening: $SUMMARY_HARDENING" log_action "security hardening: $SUMMARY_HARDENING"
log_action "manifest: $SUMMARY_MANIFEST"
log_action "перезапуск ssh-monitor.service: $SUMMARY_SERVICE_RESTART" log_action "перезапуск ssh-monitor.service: $SUMMARY_SERVICE_RESTART"
log_action "полный лог: $LOG_FILE" log_action "полный лог: $LOG_FILE"
log_action "===========================" log_action "==========================="
@@ -1270,6 +1472,7 @@ deploy_post_install() {
sync_installed_updater_from_clone() { sync_installed_updater_from_clone() {
local remote="$UPDATE_DIR/$SCRIPT_NAME/update_ssh_monitor.sh" local remote="$UPDATE_DIR/$SCRIPT_NAME/update_ssh_monitor.sh"
local self installed_path remote_sum installed_sum="" local self installed_path remote_sum installed_sum=""
local repo="$UPDATE_DIR/$SCRIPT_NAME" ver manifest_path=""
if [ ! -f "$remote" ]; then if [ ! -f "$remote" ]; then
SUMMARY_UPDATER="не найден в клоне" SUMMARY_UPDATER="не найден в клоне"
@@ -1277,6 +1480,16 @@ sync_installed_updater_from_clone() {
return 0 return 0
fi fi
ver=$(read_repo_release_version "$repo")
if [ -n "$ver" ]; then
manifest_path=$(find_release_manifest_path "$repo" "$ver" 2>/dev/null || true)
if [ -n "$manifest_path" ] && [ "${MANIFEST_VERIFIED:-0}" != "1" ]; then
SUMMARY_UPDATER="manifest не verified"
log_action "ERROR: updater: $manifest_path не verified — копирование пропущено"
return 1
fi
fi
self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")" self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")"
installed_path="$(readlink -f "$DEPLOY_INSTALL_PATH" 2>/dev/null || realpath "$DEPLOY_INSTALL_PATH" 2>/dev/null || printf '%s' "$DEPLOY_INSTALL_PATH")" installed_path="$(readlink -f "$DEPLOY_INSTALL_PATH" 2>/dev/null || realpath "$DEPLOY_INSTALL_PATH" 2>/dev/null || printf '%s' "$DEPLOY_INSTALL_PATH")"
remote_sum=$(file_sha256 "$remote") remote_sum=$(file_sha256 "$remote")
@@ -1318,6 +1531,14 @@ restart_ssh_monitor_service() {
SUMMARY_SERVICE_RESTART="выполнен" SUMMARY_SERVICE_RESTART="выполнен"
log_action "ssh-monitor.service перезапущен" log_action "ssh-monitor.service перезапущен"
debug_step "Сервис успешно перезапущен" debug_step "Сервис успешно перезапущен"
local i
for i in $(seq 1 30); do
if systemctl is-active --quiet ssh-monitor.service 2>/dev/null; then
sleep 2
break
fi
sleep 1
done
else else
SUMMARY_SERVICE_RESTART="не удался" SUMMARY_SERVICE_RESTART="не удался"
log_action "WARNING: systemctl restart ssh-monitor.service не выполнен" log_action "WARNING: systemctl restart ssh-monitor.service не выполнен"
@@ -1344,9 +1565,16 @@ update_script() {
SUMMARY_CONFIG="—" SUMMARY_CONFIG="—"
SUMMARY_GIT="—" SUMMARY_GIT="—"
SUMMARY_HARDENING="—" SUMMARY_HARDENING="—"
SUMMARY_MANIFEST="—"
SUMMARY_SERVICE_RESTART="не требовался" SUMMARY_SERVICE_RESTART="не требовался"
log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG)" load_git_verify_settings
load_ssh_monitor_perms_lib || true
if declare -F ssh_monitor_update_mark_begin >/dev/null 2>&1; then
ssh_monitor_update_mark_begin
log_action "agent-update: state file установлен (подавление lifecycle/watchdog)"
fi
log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG, git_ref=${GIT_REF:-$GIT_BRANCH})"
debug_step "Запуск функции обновления скрипта" debug_step "Запуск функции обновления скрипта"
debug_step "Гарантируем наличие директории для работы" debug_step "Гарантируем наличие директории для работы"
@@ -1375,9 +1603,10 @@ update_script() {
log_action "WARNING: git sync не удался — remote в клоне может быть устаревшим" log_action "WARNING: git sync не удался — remote в клоне может быть устаревшим"
fi fi
else else
log_action "git: clone $REPO_URL$UPDATE_DIR/$SCRIPT_NAME" local clone_ref="${GIT_REF:-$GIT_BRANCH}"
debug_step "Выполняем git clone $REPO_URL $SCRIPT_NAME" log_action "git: clone -b $clone_ref $REPO_URL$UPDATE_DIR/$SCRIPT_NAME"
if ! git clone -b "$GIT_BRANCH" "$REPO_URL" "$SCRIPT_NAME" >> "$LOG_FILE" 2>&1; then debug_step "Выполняем git clone -b $clone_ref $REPO_URL $SCRIPT_NAME"
if ! git clone -b "$clone_ref" "$REPO_URL" "$SCRIPT_NAME" >>"$LOG_FILE" 2>&1; then
SUMMARY_GIT="clone FAILED" SUMMARY_GIT="clone FAILED"
SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="git clone failed" SUMMARY_SCRIPT_REASON="git clone failed"
@@ -1385,11 +1614,25 @@ update_script() {
log_update_summary log_update_summary
return 1 return 1
fi fi
if [ -n "${GIT_REF:-}" ] && ! verify_git_ref "$git_repo" "$GIT_REF" "$GIT_VERIFY_MODE"; then
SUMMARY_GIT="verify FAILED ($GIT_REF)"
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="GIT_REF verify failed"
log_update_summary
return 1
fi
git_after="$(get_git_short_rev "$git_repo")" git_after="$(get_git_short_rev "$git_repo")"
SUMMARY_GIT="clone ok, commit $git_after" SUMMARY_GIT="clone ok, commit $git_after"
log_action "git clone: успех (commit $git_after)" log_action "git clone: успех (commit $git_after)"
fi fi
if ! verify_release_manifest "$git_repo"; then
SUMMARY_SCRIPT="ошибка"
SUMMARY_SCRIPT_REASON="manifest verify failed"
log_update_summary
return 1
fi
sync_installed_updater_from_clone "$@" sync_installed_updater_from_clone "$@"
actual_script_path="$(find_remote_ssh_monitor_path 2>/dev/null || true)" actual_script_path="$(find_remote_ssh_monitor_path 2>/dev/null || true)"
@@ -1534,6 +1777,13 @@ fi
maybe_enable_deploy_for_missing_install maybe_enable_deploy_for_missing_install
cleanup_update_state() {
if declare -F ssh_monitor_update_mark_end >/dev/null 2>&1; then
ssh_monitor_update_mark_end
fi
}
trap cleanup_update_state EXIT
debug_step "Начинаем проверку и установку ipset (только --deploy / first_deploy)" debug_step "Начинаем проверку и установку ipset (только --deploy / first_deploy)"
if [ "$DEPLOY_MODE" -eq 1 ]; then if [ "$DEPLOY_MODE" -eq 1 ]; then
if ! ensure_ipset_installed; then if ! ensure_ipset_installed; then
+1 -1
View File
@@ -1 +1 @@
2.1.9-SAC 2.2.1-SAC