From b328d32f9739a273e7dc895d7bd783116bec2900 Mon Sep 17 00:00:00 2001 From: PTah Date: Fri, 19 Jun 2026 23:25:54 +1000 Subject: [PATCH] docs: agent control plane concept (v0.9.12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Утверждённая концепция v0.7: RDG flap, qwinsta/logoff, обновления агентов и desired config. Только документация и bump версии — runtime без изменений. --- .gitignore | 1 + README.md | 2 +- README_en.md | 2 +- backend/app/version.py | 2 +- backend/tests/test_health.py | 4 +- docs/INDEX.md | 3 +- docs/agent-control-plane.md | 326 +++++++++++++++++++++++++++++++++++ docs/agent-update-backlog.md | 2 +- docs/roadmap.md | 10 +- frontend/src/version.ts | 2 +- 10 files changed, 342 insertions(+), 12 deletions(-) create mode 100644 docs/agent-control-plane.md diff --git a/.gitignore b/.gitignore index 6aceca7..6cb943a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ pgdata/ # Logs *.log +.cursorignore diff --git a/README.md b/README.md index 9a7f46e..b5915cd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Статус -**Версия:** `0.9.0` +**Версия:** `0.9.12` **Стек:** FastAPI, PostgreSQL, Vue 3, JWT, SSE **Деплой:** `sudo /opt/sac-deploy.sh` (см. `deploy/sac-deploy.sh`) diff --git a/README_en.md b/README_en.md index 735fe20..096a5bf 100644 --- a/README_en.md +++ b/README_en.md @@ -14,7 +14,7 @@ Self-hosted hub for collecting, storing, and displaying security events from Lin ## Status -**Version:** `0.9.0` +**Version:** `0.9.12` **Stack:** FastAPI, PostgreSQL, Vue 3, JWT, SSE **Deploy:** `sudo /opt/sac-deploy.sh` (see `deploy/sac-deploy.sh`) diff --git a/backend/app/version.py b/backend/app/version.py index 21e3042..d659bfe 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.9.11" +APP_VERSION = "0.9.12" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index e2f888c..e38553a 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL def test_version_constants(): - assert APP_VERSION == "0.9.11" + assert APP_VERSION == "0.9.12" assert APP_NAME == "Security Alert Center" - assert APP_VERSION_LABEL == "Security Alert Center v.0.9.11" + assert APP_VERSION_LABEL == "Security Alert Center v.0.9.12" diff --git a/docs/INDEX.md b/docs/INDEX.md index d5ebee2..f97eedf 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -18,7 +18,8 @@ | [seaca-mobile.md](seaca-mobile.md) | **Seaca**: коды, устройства, сессия, API | | [seaca-fcm.md](seaca-fcm.md) | **Seaca push**: Firebase и `SAC_FCM_*` | | [workspace-three-repos.md](workspace-three-repos.md) | Multi-root workspace (три репо) | -| [agent-update-backlog.md](agent-update-backlog.md) | **ToDo:** версии агентов, удалённое обновление | +| [agent-control-plane.md](agent-control-plane.md) | **v0.7:** RDG flap, qwinsta/logoff, обновления агентов, config push | +| [agent-update-backlog.md](agent-update-backlog.md) | Ранний backlog (см. agent-control-plane.md) | ## Порядок чтения diff --git a/docs/agent-control-plane.md b/docs/agent-control-plane.md new file mode 100644 index 0000000..54bf500 --- /dev/null +++ b/docs/agent-control-plane.md @@ -0,0 +1,326 @@ +# Agent Control Plane — концепция SAC v0.7+ + +**Статус:** утверждено для реализации (2026-06). +**Связано:** [roadmap.md](roadmap.md) §v0.7, [agent-integration.md](agent-integration.md), [agent-update-backlog.md](agent-update-backlog.md). + +--- + +## 1. Зачем + +Сейчас SAC — **односторонний** канал: агент → ingest. Для задач из [roadmap.md](roadmap.md) (§ «Добавить») нужен **обратный канал** SAC ↔ агент и управление хостами: + +| # | Задача | Кратко | +|---|--------|--------| +| П.1 | RDG flap 302→303 | Детект, Problem, кнопка **qwinsta** / **logoff** в UI | +| П.2 | Обновления агентов | Режим GPO или SAC (pull + fallback SSH/WinRM) | +| П.3 | Настройки агента | Desired config в карточке хоста → poll агента | + +**Эталонный git для prod и агентов:** [git.kalinamall.ru/PapaTramp](https://git.kalinamall.ru/PapaTramp). GitHub — публичная sanitized-копия; mirror-скрипты (`push-mirror.sh`, `Rewrite-GitHostUrls.ps1`) живут на kalinamall и нужны только для синхронизации URL в docs между remotes, к runtime агентов не относятся. + +--- + +## 2. Общая архитектура + +```mermaid +flowchart TB + subgraph sac [SAC Ubuntu] + API[FastAPI] + UI[Vue SPA] + PG[(PostgreSQL)] + VAULT[Encrypted secrets] + end + + subgraph win [Windows host] + RDP[RDP-login-monitor] + end + + subgraph linux [Linux host] + SSH[ssh-monitor] + end + + RDP -->|POST events / heartbeat| API + SSH -->|POST events / heartbeat| API + RDP -->|GET agent/commands poll| API + SSH -->|GET agent/commands poll| API + UI --> API + API --> PG + API --> VAULT +``` + +**Принципы:** + +- Агент за NAT — только **исходящий HTTPS**; inbound на хост не требуется. +- Команды и конфиг — **poll** на heartbeat (или отдельный timer 30–60 с). +- Секреты (admin password, SSH private keys, WinRM password) — **encrypted at rest** в PostgreSQL; master key в `SAC_HOST_KEY_ENCRYPTION_KEY` (env). +- Audit: все команды и результаты — события ingest `agent.command.*`, `agent.update.*`. + +--- + +## 3. П.1 — RDG flap 302→303 и qwinsta / logoff + +### 3.1. Смысл проблемы + +При неудачном RDP через RD Gateway типична пара событий Windows **302 → 303** за несколько секунд. Клиент не удерживает сессию; **«зависшая» сессия обычно остаётся на компьютере пользователя** (session host / рабочая станция из `details.internal_ip`), а не на gateway. + +SAC уже принимает: + +| Windows | SAC `type` | Пример severity | +|---------|------------|-----------------| +| 302 | `rdg.connection.success` | info / warning | +| 303 | `rdg.connection.disconnected` | info | +| 303 (alt) | `rdg.connection.failed` | warning | + +Правило должно учитывать **оба** варианта 303. + +### 3.2. Правило детекта `rule:rdg_session_flap` + +``` +WHEN event_B.type IN (rdg.connection.disconnected, rdg.connection.failed) + AND exists event_A within 1..10 seconds BEFORE event_B + AND event_A.type = rdg.connection.success + AND same host_id + AND same details.user + AND (recommended) same details.internal_ip +OPTIONAL reduce false positives: + AND event_B.details.session_duration_sec <= 15 # если агент передаёт из 303 + AND/OR low bytes transferred in 303 payload +THEN: + mark event_B.details.rdg_flap = true + create Problem rule:rdg_session_flap (dedup 30 s, см. ниже) + send notification (Telegram / email / webhook по policy) +``` + +**Порядок:** только **302 → 303**, не наоборот. + +**Окно:** **1–10 секунд** между 302 и 303. + +**Dedup Problem:** **30 секунд** по ключу `{host_id}|{user}|{internal_ip}`. Пользователи часто пробуют 1–2 раза подряд и звонят админам — 60 с слишком долго. + +**Хост:** правило **универсальное** для любого хоста с RDG-событиями в домене (любой DC / gateway / session host, где стоит агент). + +**Маршрутизация команды qwinsta:** команда уходит на хост, где **установлен агент** и куда привязано событие (`host_id`). Если session host ≠ gateway, в перспективе — таблица «gateway → session hosts» или агент на session host; MVP — агент на том же сервере, что шлёт события RDG. + +### 3.3. UI — «Обзор» → «Последние события» + +В таблице [DashboardView](../frontend/src/views/DashboardView.vue) для строки события с `rdg_flap = true` (обычно **303**, второе в паре): + +| … | Title | **Действия** | +|---|-------|--------------| +| … | RD Gateway event 303 | **[qwinsta]** | + +**Поток:** + +1. Оператор нажимает **qwinsta**. +2. `POST /api/v1/events/{id}/actions/qwinsta` → SAC ставит команду в очередь хоста. +3. Агент на poll выполняет `qwinsta` под **доменным admin** (см. §5). +4. Результат → модальное окно: таблица SESSIONNAME, USERNAME, ID, STATE. +5. **logoff:** + - по умолчанию — кнопка только у строк с **matching user** из события; + - дополнительно — возможность **выбрать любую строку** (на серверах бывает нестандартный вывод qwinsta). +6. `POST .../actions/logoff { session_id }` → подтверждение → команда агенту → `logoff {id} /v`. +7. Результат в модалке; Problem можно auto-resolve или оставить оператору. + +```mermaid +sequenceDiagram + participant Op as Оператор + participant UI as SAC UI + participant API as SAC API + participant Ag as RDP-agent + + Op->>UI: qwinsta + UI->>API: POST events/{id}/actions/qwinsta + API->>Ag: poll command qwinsta + Ag->>Ag: runas domain admin + Ag->>API: agent.command.result + API->>UI: modal + parsed sessions + Op->>UI: logoff session_id + UI->>API: POST actions/logoff + Ag->>API: agent.command.result +``` + +**Автоматический logoff без участия оператора — не делаем** в первых итерациях. + +--- + +## 4. П.2 — Обновления агентов (Windows + Linux) + +### 4.1. Настройки SAC → «Обновления агентов» + +| Режим | Описание | +|-------|----------| +| **A — GPO / ручной** | Как сейчас: NETLOGON, `Deploy-LoginMonitor.ps1`, `update_ssh_monitor.sh`. SAC только показывает устаревшие версии. | +| **B — SAC** | SAC инициирует обновление; агент тянет пакет сам (pull). | + +**Рекомендуемые версии** по продукту (`RDP-login-monitor`, `ssh-monitor`) — min / recommended; сверка с `host.product_version` (уже есть в UI «Хосты»). + +**Источник пакетов** (выбор в настройках): + +| Источник | Windows | Linux | +|----------|---------|-------| +| SMB / NETLOGON | `\\dc\NETLOGON\...` | — | +| Git | private repo kalinamall | `git pull` в `/opt/ssh-monitor` | +| HTTP(S) SAC | zip + checksum с SAC | zip + checksum | + +### 4.2. B1 — Self-update после «пинка» (основной путь) + +1. Admin: «Запросить обновление» на хосте / группе. +2. SAC: `host.pending_update = true` в ответе poll. +3. **Windows:** агент запускает `Deploy-LoginMonitor.ps1` (существующий скрипт). +4. **Linux:** агент запускает `update_ssh_monitor.sh`. +5. Ingest: `agent.update.started` → `agent.update.success` | `agent.update.failed`. + +### 4.3. B2 — Fallback (SSH / WinRM) + +Если за **N минут** нет `agent.update.*` или статус `failed`: + +| OS | Метод | Credentials | +|----|--------|-------------| +| Linux | SSH из SAC | ключ после bootstrap (§5) | +| Windows | WinRM | encrypted login/password в БД | + +--- + +## 5. П.2C — Доступ SAC к хостам + +### 5.1. Windows — WinRM + +- **Один доменный admin** в **Настройки → Управление хостами → Windows**: `DOMAIN\user` + password (encrypted). +- Override на карточке хоста — опционально позже. +- Используется для fallback-обновления и (при необходимости) remote ops; для qwinsta/logoff MVP — **через агента** с теми же creds, переданными в command poll (TLS + API key, не пишутся на диск агента). + +### 5.2. Linux — bootstrap password → SSH key → удалить password + +На карточке хоста → «Доступ для управления»: + +``` +1. Admin вводит login + password (временно) +2. SAC по SSH: генерирует ed25519, добавляет pubkey в authorized_keys +3. SAC проверяет вход по ключу +4. Успех → password DELETE из БД, status = key_ready +5. Ошибка → password остаётся, status = bootstrap_failed, алерт +6. Кнопка «Переустановить ключи» — повтор bootstrap +``` + +Private key — encrypted в PostgreSQL. + +### 5.3. Статусы доступа + +| Статус | Linux | Windows | +|--------|-------|---------| +| `no_access` | нет данных | нет WinRM | +| `bootstrap_pending` | идёт настройка ключа | — | +| `key_ready` | SSH по ключу | — | +| `winrm_ready` | — | WinRM настроен | + +--- + +## 6. П.3 — Настройки агента в карточке хоста + +**Desired state** в SAC; агент забирает на poll: + +```json +{ + "config_revision": 42, + "settings": { + "ServerDisplayName": "UNMS Kalina", + "EnableRcmShadowControlMonitoring": true, + "GetInventory": true, + "HEARTBEAT_INTERVAL": "300" + } +} +``` + +- Whitelist ключей — без секретов (`SAC_API_KEY` только локально). +- **Windows:** поля из `login_monitor.settings.ps1`. +- **Linux:** поля из `/etc/ssh-monitor.conf`. +- Merge: remote перекрывает локальное, revision монотонно растёт. + +--- + +## 7. API (черновик) + +### 7.1. Poll (агент) + +```http +GET /api/v1/agent/commands?since= +Authorization: Bearer sac_xxx +``` + +```json +{ + "config_revision": 42, + "config": { "settings": { } }, + "update": { "requested": false, "target_version": "2.0.38-SAC", "source": "smb://..." }, + "commands": [ + { "id": "uuid", "type": "qwinsta", "params": { "user": "B26\\user" } } + ] +} +``` + +### 7.2. UI actions (JWT admin) + +| Метод | Путь | Назначение | +|-------|------|------------| +| POST | `/api/v1/events/{id}/actions/qwinsta` | Запрос qwinsta | +| POST | `/api/v1/events/{id}/actions/logoff` | logoff `{ "session_id": 5 }` | +| GET | `/api/v1/events/{id}/actions/{cmd_id}` | Статус / результат команды | +| PATCH | `/api/v1/hosts/{id}/config` | Desired config | +| PATCH | `/api/v1/hosts/{id}/access` | Bootstrap / WinRM creds | +| GET/PATCH | `/api/v1/settings/agent-updates` | Режим A/B, версии, источники | +| GET/PATCH | `/api/v1/settings/host-management` | Доменный admin Windows | + +### 7.3. Ingest (новые типы) + +| `type` | Назначение | +|--------|------------| +| `agent.command.result` | stdout/stderr qwinsta, logoff | +| `agent.update.started` | начало self-update | +| `agent.update.success` | успех | +| `agent.update.failed` | ошибка | + +--- + +## 8. Фазы реализации + +| Фаза | Содержание | Репозитории | +|------|------------|-------------| +| **1** | `rule:rdg_session_flap`, Problem, notify, флаг `rdg_flap` на event | SAC | +| **2** | Колонка «Действия» на «Обзоре»; кнопка qwinsta (disabled до фазы 3) | SAC UI | +| **3** | Poll API, очередь команд, доменный admin в Settings | SAC + RDP-login-monitor | +| **4** | qwinsta modal, logoff (match user + выбор строки) | SAC UI + RDP agent | +| **5** | Settings «Обновления агентов», режим A/B | SAC | +| **6** | Self-update B1 (Windows + Linux) | SAC + оба агента | +| **7** | SSH bootstrap + WinRM fallback B2 | SAC | +| **8** | Desired config per host (П.3) | SAC + оба агента | + +**Старт разработки:** фаза **1** (детект без команд агента). + +--- + +## 9. Безопасность + +- Команды qwinsta/logoff — только **JWT admin**; audit log. +- Rate limit на actions per host. +- logoff — confirm dialog; по умолчанию только matching user. +- Password bootstrap Linux — удаляется после успеха; re-bootstrap явной кнопкой. +- WinRM password — только encrypted storage, не в логах и не в Telegram. + +--- + +## 10. Git / remotes (справка) + +| Репозиторий | Prod (kalinamall) | Public (github) | +|-------------|-------------------|-----------------| +| security-alert-center | основной | sanitized | +| RDP-login-monitor | prod paths/secrets | sanitized settings | +| ssh-monitor | mirror-скрипты + URLs | без mirror-скриптов | + +Mirror-скрипты: временно переписывают URL в docs под целевой host, пушат на remote, **откатывают** локальный main — чтобы в одной ветке не смешивать kalinamall/github URLs. + +--- + +## См. также + +- [agent-integration.md](agent-integration.md) — ingest, типы RDG +- [agent-update-backlog.md](agent-update-backlog.md) — ранний backlog (superseded деталями этого документа) +- [roadmap.md](roadmap.md) — v0.7 diff --git a/docs/agent-update-backlog.md b/docs/agent-update-backlog.md index cb202c6..9bb0562 100644 --- a/docs/agent-update-backlog.md +++ b/docs/agent-update-backlog.md @@ -1,6 +1,6 @@ # ToDo — управление версиями агентов и удалённое обновление -**Статус:** идея, требует проработки архитектуры. +**Статус:** superseded — основной документ [agent-control-plane.md](agent-control-plane.md). **Связано:** [roadmap.md](roadmap.md) (v0.7), [agent-integration.md](agent-integration.md), UI «Хосты». --- diff --git a/docs/roadmap.md b/docs/roadmap.md index 84b0018..82fbd5d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -91,15 +91,17 @@ - Команда обновления (предпочтительно **pull** с хоста: агент видит «нужно обновиться» и запускает свой updater) - События `agent.update.*` в ingest для аудита -**Проработка:** [agent-update-backlog.md](agent-update-backlog.md). +**Проработка:** [agent-control-plane.md](agent-control-plane.md) (основной документ), [agent-update-backlog.md](agent-update-backlog.md). --- ## Добавить -- При парах событий 302/303 за период 5-10 сек отправлять на агентский компьютер qwinsta / logoff для "зависшей" или сбрасывающей коннект сессии пользователя. -- Устанавливать новые версии агентов на целевые компьютеры помипо GPO и самим SAC. -- В разделе "Хосты" в карточке каждого компьютера сделать возможность настройки параметров агента/монитора и передачи этих настроек из SAC в агента +См. [agent-control-plane.md](agent-control-plane.md): + +- RDG flap 302→303 (1–10 с): Problem, оповещение, кнопки **qwinsta** / **logoff** на «Обзоре» +- Обновления агентов: режим GPO или SAC (pull + fallback SSH/WinRM), Windows + Linux +- Настройки агента в карточке хоста (desired config → poll) --- diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 6e238db..a8b4ae3 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.9.11"; +export const APP_VERSION = "0.9.12"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;