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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
+72
-11
@@ -26,19 +26,22 @@ Watchdog и таймеры watchdog **не изменяются** при обы
|
|||||||
|
|
||||||
1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`).
|
1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`).
|
||||||
2. При **`--deploy`** / bootstrap SAC проверяет **`ipset`** и при отсутствии пытается установить через пакетный менеджер. Обычное обновление **не** вызывает `apt-get`.
|
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`).
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A[SAC или cron] --> B[update_ssh_monitor.sh]
|
A[SAC или cron] --> B[update_ssh_monitor.sh]
|
||||||
|
B --> C[git fetch/checkout]
|
||||||
**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом update в **существующий** конфиг **дописываются** отсутствующие SAC-переменные и ключи безопасности; **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**.
|
C --> M{manifest OK?}
|
||||||
|
M -->|нет| X[exit 1]
|
||||||
|
M -->|да/skip| D[sync sac-client + perms lib]
|
||||||
C --> E{SHA256 ssh-monitor?}
|
C --> E{SHA256 ssh-monitor?}
|
||||||
D --> F[harden config/spool/state]
|
D --> F[harden config/spool/state]
|
||||||
E -->|да| G[backup + cp ssh-monitor]
|
E -->|да| G[backup + cp ssh-monitor]
|
||||||
@@ -55,12 +58,70 @@ flowchart LR
|
|||||||
|------------|--------------|------------|
|
|------------|--------------|------------|
|
||||||
| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона |
|
| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона |
|
||||||
| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron |
|
| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron |
|
||||||
|
| `GIT_BRANCH` | `main` | Ветка для clone/pull, если **`GIT_REF`** не задан |
|
||||||
|
| `GIT_REF` | *(пусто)* | Тег (`2.2.0-SAC`), ветка или commit SHA для checkout |
|
||||||
|
| `GIT_VERIFY_MODE` | `off` | `off` \| `tag` \| `commit` — см. раздел ниже |
|
||||||
|
| `GIT_ALLOW_RESET` | `0` | `1` = разрешить `git reset --hard` при расхождении истории (риск) |
|
||||||
|
| `RELEASE_MANIFEST_STRICT` | `0` | `1` = exit 1, если manifest для версии не найден |
|
||||||
| `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` | Лог обновлений |
|
||||||
| `UPDATE_VIA_SAC` | *(опционально)* | SAC выставляет `1`; при отсутствии бинарника включается `--deploy` |
|
| `UPDATE_VIA_SAC` | *(опционально)* | SAC выставляет `1`; при отсутствии бинарника включается `--deploy` |
|
||||||
|
|
||||||
6. **Всегда** синхронизирует **`sac-client.sh`**, **`ssh-monitor-perms.sh`**, watchdog.
|
Требования: **root**, **git**, **python3** (manifest verify), доступ к репозиторию.
|
||||||
|
|
||||||
|
## Pinned ref и manifest (2.2.0+)
|
||||||
|
|
||||||
|
Updater читает **`GIT_REF`**, **`GIT_VERIFY_MODE`**, **`GIT_ALLOW_RESET`** из env или `/etc/ssh-monitor.conf`.
|
||||||
|
|
||||||
|
### Режимы `GIT_VERIFY_MODE`
|
||||||
|
|
||||||
|
| Режим | Поведение | Когда |
|
||||||
|
|-------|-----------|-------|
|
||||||
|
| **`off`** | После fetch принять ref как есть | Тест, legacy; дефолт в скрипте |
|
||||||
|
| **`tag`** | `GIT_REF` должен существовать как тег | **Рекомендуется для прода** |
|
||||||
|
| **`commit`** | `GIT_REF` = полный SHA commit | Жёсткий pin без тегов |
|
||||||
|
|
||||||
|
Если **`GIT_REF`** пуст, используется **`GIT_BRANCH`** (`git pull --ff-only`). При невозможности fast-forward updater **не** делает `reset --hard` (только с **`GIT_ALLOW_RESET=1`**).
|
||||||
|
|
||||||
|
### Release manifest
|
||||||
|
|
||||||
|
В репозитории: **`release/manifest-VERSION.json`** — SHA256 ключевых файлов и `git_commit`. Updater сверяет manifest **после** git sync и **до** копирования бинарников.
|
||||||
|
|
||||||
|
- Файл найден и hash не совпал → **exit 1**, ничего не перезаписывается.
|
||||||
|
- Файл не найден → WARN, update продолжается (или exit 1 при **`RELEASE_MANIFEST_STRICT=1`**).
|
||||||
|
- Копирование **`update_ssh_monitor.sh`** в `/opt/scripts/` и re-exec — только если manifest для версии был и прошёл verify.
|
||||||
|
|
||||||
|
Генерация при релизе (после **финального** коммита кода, чтобы `git_commit` в manifest совпал с HEAD):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**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)
|
## Первичная установка (--deploy)
|
||||||
|
|
||||||
|
|||||||
+21
-20
@@ -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.7–2.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.7–2.1.9** — фаза 1 + SAC-first + watchdog server label
|
- [x] **Релиз 2.1.7–2.1.9** — фаза 1 + SAC-first + watchdog server label
|
||||||
- [ ] **Релиз 2.2.0** — scope: 2.1–2.3, 2.5, 4.5, 4.7(M7); **2.4 GPG отложить**
|
- [x] **Релиз 2.2.0** — scope: 2.1–2.3, 2.5, 4.5, 4.7(M7); **2.4 GPG отложить**
|
||||||
- [ ] **Релиз 2.3.0** — scope: 3.1–3.3, удаление webhook (если A)
|
- [ ] **Релиз 2.3.0** — scope: 3.1–3.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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+15
-2
@@ -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
|
||||||
|
python3 -c 'import json,sys; print(json.dumps({"ts": int(sys.argv[1]), "hostname": sys.argv[2], "dry_run": sys.argv[3] == "1"}))' \
|
||||||
|
"$ts" "$(hostname)" "${DRY_RUN:-0}" >"${HEALTHCHECK_STATUS_FILE}.tmp"
|
||||||
ssh_monitor_secure_file "${HEALTHCHECK_STATUS_FILE}.tmp" 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
|
mv "${HEALTHCHECK_STATUS_FILE}.tmp" "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true
|
||||||
ssh_monitor_secure_file "$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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+260
-10
@@ -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,10 +626,172 @@ 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}"
|
||||||
@@ -633,6 +802,31 @@ sync_git_repository() {
|
|||||||
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)"
|
||||||
@@ -647,7 +841,8 @@ sync_git_repository() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_action "WARN: fast-forward невозможен (часто после force-push) — reset --hard $upstream"
|
if [ "$GIT_ALLOW_RESET" -eq 1 ]; then
|
||||||
|
log_action "WARN: fast-forward невозможен — GIT_ALLOW_RESET=1, reset --hard $upstream"
|
||||||
if ! git -C "$repo" reset --hard "$upstream" >>"$LOG_FILE" 2>&1; then
|
if ! git -C "$repo" reset --hard "$upstream" >>"$LOG_FILE" 2>&1; then
|
||||||
SUMMARY_GIT="reset FAILED ($git_before)"
|
SUMMARY_GIT="reset FAILED ($git_before)"
|
||||||
log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)"
|
log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)"
|
||||||
@@ -657,6 +852,12 @@ sync_git_repository() {
|
|||||||
SUMMARY_GIT="reset ok, $git_before → $git_after"
|
SUMMARY_GIT="reset ok, $git_before → $git_after"
|
||||||
log_action "git reset --hard: $git_before → $git_after"
|
log_action "git reset --hard: $git_before → $git_after"
|
||||||
return 0
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUMMARY_GIT="diverged ($git_before)"
|
||||||
|
log_action "ERROR: git: история разошлась (ff-only невозможен). Ручной clone или GIT_ALLOW_RESET=1"
|
||||||
|
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
@@ -1 +1 @@
|
|||||||
2.1.9-SAC
|
2.2.1-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user