# 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 — веб и Seaca Бейдж **RDG flap** и кнопка **qwinsta** на событиях **302 и 303** (пара вычисляется при чтении): - Веб: **Обзор**, **События**, карточка события - **Seaca:** список событий + карточка → `POST .../actions/qwinsta` / `logoff` **Поток:** 1. Оператор нажимает **qwinsta**. 2. `POST /api/v1/events/{id}/actions/qwinsta` → SAC выполняет **WinRM qwinsta** на **клиентский ПК** (`internal_ip` из события). 3. Результат — модалка: SESSION, USER, ID, STATE. 4. **logoff:** `POST .../actions/logoff { session_id }` → WinRM `logoff` → повторный qwinsta. 5. Нужен **Windows domain admin** в настройках SAC (`SAC_WIN_ADMIN_*` или UI). ```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 / monitor / mobile) | Метод | Путь | Назначение | |-------|------|------------| | POST | `/api/v1/events/{id}/actions/qwinsta` | qwinsta (WinRM на клиентский ПК) | | 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` | ✅ SAC | | **2** | **qwinsta** / **logoff** — веб + Seaca | ✅ | | **3** | Poll API, `agent_commands` (агент) | ✅ RDP ≥ 2.1.0-SAC | | **4** | WinRM qwinsta с сервера SAC | ✅ | | **5** | Settings «Обновления агентов», SSH bootstrap | частично ✅ | | **6** | Self-update B1 | ⏳ | | **7** | SSH bootstrap + WinRM fallback | ⏳ | | **8** | Desired config per host | ⏳ | **Миграция:** `016_agent_commands`. Env: `SAC_RDG_FLAP_*`, `SAC_WIN_ADMIN_USER`, `SAC_WIN_ADMIN_PASSWORD`. --- ## 9. Безопасность - Команды qwinsta/logoff — JWT (веб и Seaca); `requested_by` в `agent_commands`. - 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