Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)
This commit is contained in:
+19
-8
@@ -14,13 +14,14 @@
|
||||
| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, nginx, сетям, TLS и rate limits |
|
||||
| [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | `.env` (infra), таблица `app_settings`, значения service-token переменных, типы файлов |
|
||||
| [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md) | Правила разработки модулей отдельными агентами |
|
||||
| [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md) | Безопасность VM и деплоя: OS-роли, SSH, sudo/systemd, секреты, контейнеры, сеть и lockdown |
|
||||
|
||||
## Как читать
|
||||
|
||||
1. Начните с **arch-01** — общая картина и зафиксированные решения MVP.
|
||||
2. При работе с API — **arch-02**; при деплое — **arch-03**; при настройках — **arch-04**.
|
||||
2. При работе с API — **arch-02**; с Compose/nginx — **arch-03**; с настройками — **arch-04**; с VM, SSH, правами деплоя, секретами и host/container hardening — **arch-06**.
|
||||
3. Спорные **имена** полей, id, enum, бакетов и базовая семантика enum/lifecycle — **arch-00**. Лимиты и правила реализации остаются в профильных arch-*.
|
||||
4. Перед разработкой модуля — **arch-05** и релевантные разделы arch-01/arch-02.
|
||||
4. Перед разработкой модуля — **arch-05**, релевантные разделы arch-01/arch-02 и arch-06, если меняются deployment, сети, volumes, capabilities или секреты.
|
||||
|
||||
## Приоритет документов
|
||||
|
||||
@@ -29,9 +30,10 @@
|
||||
1. **arch-00** — **имена и базовая семантика** (поля, id, enum, бакеты, env, смысл статусов); не бизнес-лимиты и не детальная реализация.
|
||||
2. **arch-01** — границы сервисов, сценарии, sync, безопасность.
|
||||
3. **arch-02** — HTTP-контракты и направление вызовов.
|
||||
4. **arch-03** — инфраструктура и nginx.
|
||||
5. **arch-04** — env, `app_settings`, публичные DTO.
|
||||
6. **arch-05** — процесс разработки.
|
||||
4. **arch-06** — безопасность размещения на VM, OS-роли, SSH, secrets delivery, host/container hardening и production-деплой.
|
||||
5. **arch-03** — Compose, сети контейнеров, nginx и TLS.
|
||||
6. **arch-04** — non-secret env, secret references, `app_settings`, публичные DTO.
|
||||
7. **arch-05** — процесс разработки.
|
||||
|
||||
Профильные спецификации модулей уточняют реализацию внутри этих границ. Если границы не позволяют эффективно реализовать модуль, то агент, разрабатывающий модуль, может предложить внести изменения в архитектуру.
|
||||
|
||||
@@ -41,6 +43,7 @@
|
||||
- Endpoint или auth → **arch-02**, при необходимости arch-01/arch-03.
|
||||
- Новая интеграция → сначала **arch-02**.
|
||||
- Compose, nginx, TLS → **arch-03**.
|
||||
- VM, SSH, sudo, systemd-деплой, secret delivery, container/host hardening → **arch-06**, затем синхронизация arch-03/arch-04 и runbook.
|
||||
|
||||
## В бэклоге (не MVP)
|
||||
|
||||
@@ -48,7 +51,14 @@
|
||||
|---|---|
|
||||
| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» |
|
||||
| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | Спецификация: [`module-11-idgtl-sms.md`](../modules/module-11-idgtl-sms.md) (доставка через Direct SMS API; проверка OTP — локально в Keycloak) |
|
||||
| Изоляция `bitrix-sync` на отдельную VM | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 8 |
|
||||
|
||||
## Каноническое размещение production-контуров
|
||||
|
||||
- **ВМ1 HAN Chat** — самостоятельная публичная точка входа приложения: nginx, `api-backend`, Keycloak, `bitrix-local-app`, SMS-контур, Redis DB0/DB1 и локальный OTEL Collector.
|
||||
- **ВМ2 Processing** — самостоятельная service VM с отдельным public webhook host, private Message Safety ingress и постоянным ограниченным egress: `message-safety`, `bitrix-sync`, `clamd`/`freshclam`, отдельный Redis Safety, nginx и локальный OTEL Collector.
|
||||
- На каждой VM действует один root Compose project и отдельный root-owned systemd deployment unit. «Единый Compose» означает один проект **на VM**, а не один общий project через несколько хостов.
|
||||
- Bitrix24 вызывает CRM webhook напрямую на nginx ВМ2; ВМ1 в route не участвует. ВМ1 вызывает только Message Safety по private HTTPS.
|
||||
- При росте нагрузки `bitrix-sync` может быть перенесён на ВМ3 без изменения API и границ схем PostgreSQL.
|
||||
|
||||
|
||||
## Открытые пробелы
|
||||
@@ -56,15 +66,16 @@
|
||||
| # | Пробел | Статус |
|
||||
|---|---|---|
|
||||
| G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` |
|
||||
| G9 | GRANT-модель `bitrix_sync_user` на `han_app`: таблицы, колонки, read/write границы | Уточнить в спецификации `database` и `bitrix-sync` |
|
||||
| G9 | GRANT-модель `bitrix_sync_user` на `han_app`: таблицы, колонки, read/write границы | Закрыто в `module-07-bitrix-sync.md` §13; точный SQL реализуется migrations и проходит negative permission tests |
|
||||
| G10 | Полный DTO `GET /api/v1/public/app-config` и мэппинг `setting_key → response field` | Уточнить при оформлении OpenAPI `api-backend` |
|
||||
| G11 | Версионирование API/WS: deprecation policy, срок поддержки v1, `ws_protocol_version` | Уточнить перед публичным релизом API |
|
||||
| G12 | Масштабирование realtime: Redis Pub/Sub, sticky sessions, backpressure при нескольких репликах `api-backend` | Post-MVP / перед горизонтальным масштабированием |
|
||||
| G13 | Contract tests между `api-backend`, `message-safety`, `bitrix-local-app`, `bitrix-sync` | Добавить в DoD модулей после появления OpenAPI |
|
||||
| G13 | Contract tests между `api-backend`, `message-safety`, `bitrix-local-app`, `bitrix-sync` | Контракт sync зафиксирован в module-07 §18; общий межсервисный gate остаётся до появления всех OpenAPI |
|
||||
|
||||
## Обновление документации
|
||||
|
||||
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
|
||||
- Новый env или ключ `app_settings` → arch-04.
|
||||
- Новая VM, изменение сетевой доступности, прав `deploy`, sudo/systemd, capabilities, volumes или способа доставки секретов → arch-06 (+ arch-03/arch-04 и deployment runbook).
|
||||
- Новый термин / enum → arch-00, затем поиск по arch-*.
|
||||
- Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в arch-*.
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
| `Dialog` | `han_app` | Диалог клиента с Open Lines |
|
||||
| `Message` | `han_app` | Сообщение в диалоге |
|
||||
| `MessageAttachment` | `han_app` | Вложение к сообщению |
|
||||
| `sync_queue` | `han_app` | Очередь sync App → Bitrix24 |
|
||||
| `sync_queue` | `han_app` | Durable очередь бизнес-намерений App → Bitrix24 с lease/fencing |
|
||||
| `safety_tasks` | `han_app` | Checkpoint sync-wait Message Safety (`task_id`) для recovery |
|
||||
| `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity |
|
||||
| `app_settings` | `han_app` | Бизнес-настройки |
|
||||
| `text_resources` | `han_app` | Тексты UI по мнемоникам |
|
||||
| `popular_questions` | `han_app` | Популярные вопросы главного экрана |
|
||||
@@ -39,6 +38,10 @@
|
||||
| `NotificationSource` | `han_app` | Продюсер Internal Notifications API; хранит hash индивидуального токена, не секрет |
|
||||
| `ClientDocument` | `han_app` | Отправленный клиентом проверенный документ; создаёт `document.client_uploaded` в `sync_queue` |
|
||||
| `dialog_sessions` | `bitrix_local` | Маппинг чата Open Lines |
|
||||
| `entity_external_mapping` | `bitrix_sync` | Каноническая active/closed/broken история `user_id` ↔ Bitrix Contact |
|
||||
| `workflow_instances` / `crm_commands` | `bitrix_sync` | Persisted сценарии CRM sync и конкретные Bitrix batch subcommands |
|
||||
| `webhook_inbox` | `bitrix_sync` | Durable inbox событий Contact/smart process от Битрикс24 |
|
||||
| `business_alerts` | `bitrix_sync` | Локальное состояние конфликтов, связанных со smart process Битрикс24 |
|
||||
| `sms_template` | `sms` | Версионируемый согласованный SMS-шаблон; active-версия уникальна для `code`+`channel`+`locale` |
|
||||
| `sms_setting` | `sms` | Технические runtime-настройки `sms-service`, не секреты и не OTP product settings |
|
||||
| `sms_outbound_message` | `sms` | Бессрочный журнал заказа, отправки и доставки SMS; источник истины provider status |
|
||||
@@ -53,7 +56,7 @@
|
||||
| `phone_number` | Auth-телефон пользователя; master — Keycloak; в App DB пишется из JWT claims при `bootstrap`, не из body клиента |
|
||||
| `guest_session_id` | Опциональный локальный UUID на устройстве (UI); **не** auth и **не** открывает write API |
|
||||
| `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` |
|
||||
| `bitrix_contact_id` | ID Contact в Bitrix24 CRM |
|
||||
| `b24_id` | ID Contact в Bitrix24 CRM; хранится только в schema `bitrix_sync` |
|
||||
| `bitrix_chat_id` | ID чата Open Lines в Bitrix24 |
|
||||
| `session_id` | ID сессии Open Lines (поле `dialog_sessions`; не путать с `ux_session_id`) |
|
||||
| `task_id` | ID async-проверки Message Safety |
|
||||
@@ -111,11 +114,13 @@
|
||||
|---|---|
|
||||
| `Message.sender_type` | `client`, `company` |
|
||||
| `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) |
|
||||
| `Message.safety_processing_mode` | `standard`, `mock`; internal/audit field, не public DTO |
|
||||
| `Message.safety_config_version` | версия service-owned Message Safety config, internal/audit field |
|
||||
| `Message.delivery_status` | `accepted`, `processing`, `delivered`, `rejected`, `failed` |
|
||||
| `Message.text` | текст сообщения; пустая строка для файлового сообщения |
|
||||
| `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) |
|
||||
|
||||
Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||
Семантика `allow` / `deny` / `pending` в target Message Safety v2: `200` / `403` / `202 Accepted`; internal `202` скрыт api-backend от public API. `/internal/safety/v1/*`, `203` и `stub_final_error` — только legacy test stub до cutover. Полный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||
|
||||
### `Message.delivery_status` (смысл)
|
||||
|
||||
@@ -135,6 +140,7 @@ Realtime-событие `message.status` передаёт актуальные `
|
||||
|---|---|
|
||||
| `pending` | Файл в S3-quarantine, проверка не завершена |
|
||||
| `clean` | Проверка завершена, allow |
|
||||
| `bypassed` | Forced allow в emergency MOCK; файл не проверялся |
|
||||
| `infected` | Проверка завершена, deny |
|
||||
| `failed` | Ошибка инфраструктуры проверки |
|
||||
|
||||
@@ -152,7 +158,7 @@ Realtime-событие `message.status` передаёт актуальные `
|
||||
|
||||
## Мнемоники internal API
|
||||
|
||||
Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**.
|
||||
Default-префикс: **`/internal/{service_mnemonic}/v1/`**. Approved exception: target Message Safety использует **`/internal/safety/v2/`**; `/v1` остаётся legacy stub до cutover. Health: **`/health/*`**.
|
||||
|
||||
| `{service_mnemonic}` | Сервис |
|
||||
|---|---|
|
||||
@@ -163,6 +169,8 @@ Realtime-событие `message.status` передаёт актуальные `
|
||||
| `sms` | `sms-service`; durable order/read API во внутренней сети |
|
||||
| `notifications` | Internal Create/Cancel уведомлений на `api-backend`; токен отдельный для каждого `source` |
|
||||
|
||||
Public safety deny: internal `403 reason_code=message_blocked` → public `422 message_blocked`; `rule_id` не раскрывается. Generic company-текст берётся из `text_resources` по мнемонике `safety.chat.blocked`.
|
||||
|
||||
## Жизненный цикл уведомления
|
||||
|
||||
- `lifecycle_status`: `active` / `closed`; бизнес-завершение, не soft delete.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md).
|
||||
> Приоритет документов — в [`README.md`](README.md).
|
||||
> Безопасность размещения на VM, OS-роли, SSH, секреты и production-деплой — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||||
|
||||
## Назначение
|
||||
|
||||
@@ -47,8 +48,8 @@ HAN Chat - приложение для мигрантов, где стартов
|
||||
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
|
||||
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
|
||||
- Notification producers: сервисы приватной сети, создающие/отменяющие персональные уведомления через Internal API с отдельным Bearer token на `source`; `producer_test` используется только для smoke API.
|
||||
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
|
||||
- Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
|
||||
- Nginx Reverse Proxy: независимые точки входа ВМ1 и ВМ2; ВМ1 обслуживает приложение/Open Lines, ВМ2 — CRM webhook `bitrix-sync` и private Message Safety API.
|
||||
- Message Safety Service: отдельный сервис ВМ2 проверки исходящих сообщений; target v2 → `200 allow` | `403 deny` | `202 pending` + `Location`.
|
||||
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id` ↔ `bitrix_chat_id`.
|
||||
- Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
|
||||
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety`, `sms` — отдельный DB-user на схему.
|
||||
@@ -59,26 +60,50 @@ HAN Chat - приложение для мигрантов, где стартов
|
||||
|
||||
## Инфраструктура развёртывания (зафиксировано)
|
||||
|
||||
На первом этапе весь backend-контур работает на **одной VM** в облаке провайдера:
|
||||
Production-like backend разделён на два контура в одной private network/VPC:
|
||||
|
||||
- `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` — в Docker Compose на VM;
|
||||
- публичный доступ из интернета только через `nginx` (порты 80/443);
|
||||
- внутренние сервисы общаются по Docker-сети на localhost VM.
|
||||
- **ВМ1 HAN Chat**: edge `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1 и локальный `otel-collector`;
|
||||
- **ВМ2 Processing**: собственный public/private `nginx`, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, отдельный Redis Safety и локальный `otel-collector`;
|
||||
- каждая VM имеет один root Compose project и отдельный root-owned systemd deployment unit;
|
||||
- ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress на своих nginx; ВМ2 публикует только exact CRM webhook;
|
||||
- ВМ1 вызывает ВМ2 по private HTTPS с проверкой internal CA, service token, cloud SG и host firewall;
|
||||
- Битрикс24 вызывает public nginx ВМ2 напрямую; CRM webhook не проходит через ВМ1 и не создаёт на ней трафик/зависимость.
|
||||
|
||||
ВМ2 является независимым контуром вспомогательных сервисов. При её недоступности отправка пользовательских сообщений и CRM sync приостанавливаются, но чтение истории, auth, realtime и приём сообщений оператора на ВМ1 продолжаются. Недоступность ВМ1 не мешает ВМ2 принимать CRM webhook и выполнять накопленные workflows. Fail-open для Message Safety запрещён.
|
||||
|
||||
Базы данных — **managed PostgreSQL** того же провайдера в **том же облачном кластере/VPC**, **без публичного доступа** из интернета. VM подключается к БД только по приватной сети.
|
||||
|
||||
Размещение нескольких сервисов на одной VM не делает их одним доверенным контуром. Для каждого контейнера сохраняются least privilege, отдельные секреты, минимальные Docker networks и запрет доступа к Docker socket/host root. Обязательный baseline VM и контейнеров — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||||
|
||||
Схема данных в managed PostgreSQL (перечень таблиц внутри схем — в модульных спецификациях, не в arch-*):
|
||||
|
||||
| База / схема | Сервисы | Назначение схемы |
|
||||
|---|---|---|
|
||||
| одна база / `han_app` | `api-backend`, `bitrix-sync` (ограниченный GRANT) | прикладные данные приложения, очередь sync, audit |
|
||||
| одна база / `bitrix_sync` | `bitrix-sync` | worker state, retry/dead letter, sync audit |
|
||||
| одна база / `message_safety` | `message-safety` | verdict cache, safety_task, rule config |
|
||||
| одна база / `message_safety` | `message-safety` | verdict caches, safety tasks/audit, immutable versioned runtime config |
|
||||
| одна база / `bitrix_local` | `bitrix-local-app` | OAuth, inbox, `dialog_sessions` |
|
||||
| одна база / `keycloak` | Keycloak | учётные записи, realm, сессии IdP |
|
||||
| одна база / `sms` | `sms-service`, `sms-worker` | шаблоны, runtime settings, бессрочный журнал отправки/доставки SMS |
|
||||
|
||||
Redis на первом этапе остаётся на VM в Docker (ephemeral/coordination). Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
|
||||
Redis разделён по deployment boundary: DB0/DB1 остаются на ВМ1, отдельный Redis Safety находится на ВМ2. Оба являются ephemeral/coordination слоями; PostgreSQL остаётся durable source of truth. Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
client[Client] --> edge[VM1_EdgeNginx]
|
||||
edge --> api[VM1_ApiBackend]
|
||||
bitrix[Bitrix24] -->|"CRM webhook HTTPS"| publicGateway[VM2_PublicNginx]
|
||||
publicGateway --> sync[BitrixSync]
|
||||
api -->|"HTTPS 8443 + service token"| privateGateway[VM2_PrivateListener]
|
||||
privateGateway --> safety[MessageSafetyApi]
|
||||
safety --> worker[SafetyWorker]
|
||||
worker --> clamd[Clamd]
|
||||
worker --> s3q[S3Quarantine]
|
||||
worker --> pg[ManagedPostgreSQL]
|
||||
sync --> pg
|
||||
sync --> bitrix
|
||||
collector[VM2_OtelCollector] --> signoz[PrivateSigNoz]
|
||||
```
|
||||
|
||||
## Контекстная схема
|
||||
|
||||
@@ -179,10 +204,10 @@ Frontend не должен:
|
||||
- хранение истории диалогов;
|
||||
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue` → `bitrix-sync`, без участия api-backend);
|
||||
- выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
|
||||
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
|
||||
- вызов Message Safety v2 (`POST /internal/safety/v2/messages/check`) и интерпретацию `200 allow`, `403 deny`, `202 pending`;
|
||||
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
|
||||
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
|
||||
- при `203`: api-backend **синхронно поллит** `GET /internal/safety/v1/messages/tasks/{task_id}` до финального `200`/`403` (timeout budget — arch-04), затем promote/Bitrix или cleanup, и только после этого отвечает клиенту финальным результатом;
|
||||
- при `202`: api-backend **синхронно поллит** `Location` до финального `200`/`403`, terminal failed или timeout, затем promote/Bitrix или cleanup;
|
||||
- это **ожидание в рамках одного клиентского HTTP-соединения**, а не общая очередь: другие запросы обрабатываются параллельно (workers/async);
|
||||
- решение «быстрая проверка / долгая» принимает только `message-safety`; на api-backend **нет** очереди анализа сообщений;
|
||||
- запись checkpoint в `safety_tasks` (App DB) на время poll — для recovery при timeout/crash (I1);
|
||||
@@ -218,13 +243,15 @@ Frontend не должен:
|
||||
|
||||
### Bitrix24 sync service
|
||||
|
||||
Отвечает за **двустороннюю** синхронизацию данных между App DB и Битрикс24 CRM:
|
||||
Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md):
|
||||
|
||||
- **маппинг ID** сущностей приложения ↔ Bitrix24 (`bitrix_contact_id`, `entity_external_mapping`);
|
||||
- **App DB → Bitrix24:** обработка очереди `sync_queue` (триггеры App DB) — map/create Contact по телефону, push обновлений полей;
|
||||
- **Bitrix24 → App DB:** приём webhook от роботов Bitrix24, обновление профиля с GUC `han.sync_suppress`;
|
||||
- реестр синхронизируемых сущностей (MVP: Contact; post-MVP: Lead, Deal, Document);
|
||||
- повторные попытки, rate limiting Bitrix REST, dead letter;
|
||||
- **канонический mapping** и его историю в `bitrix_sync.entity_external_mapping`; App DB не хранит CRM Contact ID;
|
||||
- **App DB → Bitrix24:** durable workflow для `contact.map_or_create`, `contact.update`, `contact.deactivate`;
|
||||
- **исправление связи:** audited административный запрос запускает `contact.rebind`; прямой `UPDATE` mapping запрещён;
|
||||
- **Bitrix24 → App DB:** durable webhook inbox, coalescing и reconciliation; запись профиля с transaction-local GUC `han.sync_suppress`;
|
||||
- mastership по полям: телефон — App/Keycloak, `NAME`/citizenship/email — Битрикс24;
|
||||
- batch, общий portal rate limiter, leases/fencing, retry до 24 часов и technical DLQ;
|
||||
- business conflicts через смарт-процесс Битрикс24, technical failures через SigNoz;
|
||||
- прямой доступ к схеме `han_app` и собственной `bitrix_sync`.
|
||||
|
||||
Не отвечает за:
|
||||
@@ -290,12 +317,12 @@ Confidential **backend client** Keycloak (client credentials) в MVP **не об
|
||||
- HTTP-контракт для api-backend:
|
||||
- `200` — синхронная проверка завершена, **allow**;
|
||||
- `403` — синхронная проверка завершена, **deny**;
|
||||
- `203` + `task_id` — нужна async-проверка (обычно файлы), сообщение в обработке;
|
||||
- финальный вердикт async-задачи по `GET /internal/safety/v1/messages/tasks/{task_id}`: `200 allow` | `403 deny` | `203 pending`;
|
||||
- `202` + `task_id`/`Location` — нужна async-проверка;
|
||||
- task GET: `200 allow` | `403 deny` | `202 pending` | terminal failed `503`;
|
||||
- SHA-256 хеширование и lookup кэша вердиктов;
|
||||
- отдельный pipeline проверки ссылок;
|
||||
- запись verdict cache, `safety_task` и audit в схеме `message_safety`;
|
||||
- internal API: `POST /internal/safety/v1/messages/check`, `GET /internal/safety/v1/messages/tasks/{task_id}`.
|
||||
- запись verdict caches, `safety_task`, audit и immutable `config_versions` в схеме `message_safety`; runtime role не активирует config;
|
||||
- target internal API: `POST /internal/safety/v2/messages/check`, `GET /internal/safety/v2/messages/tasks/{task_id}`.
|
||||
|
||||
Не отвечает за:
|
||||
|
||||
@@ -423,7 +450,7 @@ api-backend не решает, sync или async нужна проверка в
|
||||
|
||||
1. Frontend вызывает `POST /api/v1/dialogs` (если `dialog_id` ещё нет), затем отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с непустым `text` (без вложения).
|
||||
2. Nginx и API применяют rate limits.
|
||||
3. API **синхронно** вызывает Message Safety Service (`POST /internal/safety/v1/messages/check`) — шаги текст и ссылки.
|
||||
3. API вызывает Message Safety v2 (`POST /internal/safety/v2/messages/check`) — шаги text/local links.
|
||||
4. Далее — общая ветка вердикта (п. 5–8 ниже).
|
||||
|
||||
**Файловое сообщение:**
|
||||
@@ -437,7 +464,7 @@ api-backend не решает, sync или async нужна проверка в
|
||||
|
||||
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
||||
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=accepted`) и фиксирует задачу доставки в Open Lines. После успешной отправки через `bitrix-local-app` статус становится `delivery_status=delivered`, API подтверждает клиенту финальный результат; `Dialog.status` → `waiting_for_company`. Если Bitrix24/S3/dependency недоступны после allow, статус становится `delivery_status=failed`, клиент получает безопасную ошибку зависимости.
|
||||
7. **`203 pending` + `task_id`**: api-backend пишет checkpoint в `safety_tasks` и **регулярно синхронно** вызывает `GET /internal/safety/v1/messages/tasks/{task_id}` (backoff), пока не получит финальный вердикт или не истечёт `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Пока идёт poll, **этот** клиентский `POST .../messages` ещё не завершён (соединение ждёт). Параллельные запросы других клиентов **не** блокируются — общей очереди анализа на api-backend нет.
|
||||
7. **`202 pending`**: api-backend пишет checkpoint с `Location` и синхронно поллит его с `Retry-After`, пока не получит финальный вердикт/terminal failure или не истечёт budget. Public POST остаётся открытым; другие запросы не блокируются.
|
||||
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
|
||||
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
|
||||
- timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
|
||||
@@ -448,7 +475,7 @@ api-backend не решает, sync или async нужна проверка в
|
||||
|
||||
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись `Message` и запись намерения доставки фиксируются атомарно, а повторная отправка в `bitrix-local-app` идемпотентна по `message_id` / `Idempotency-Key`.
|
||||
- `delivery_status=accepted` означает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines. `delivery_status=delivered` выставляется только после успешного ответа `bitrix-local-app` о приёме сообщения для Bitrix24 Open Lines.
|
||||
- Recovery по `han_app.safety_tasks` восстанавливает только сценарии, где Message Safety вернул `203 pending` и клиентский запрос оборвался из-за timeout/crash. Recovery job повторно опрашивает `message-safety` по `task_id`, затем идемпотентно выполняет promote/delete quarantine и обновляет `Message`/`MessageAttachment`.
|
||||
- Recovery по `han_app.safety_tasks` восстанавливает сценарии `202 pending` после timeout/crash, опрашивает сохранённый `Location`, затем идемпотентно выполняет conditional promote/delete и обновляет App DB.
|
||||
- Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного `safety_tasks` или attachment metadata.
|
||||
|
||||
## Поток работы с чатом: Битрикс24 -> клиент
|
||||
@@ -495,11 +522,11 @@ Notification Center v1 регистрирует переданные продю
|
||||
|
||||
App DB — **локальный кэш** для UI. Двусторонний sync — `bitrix-sync` (имена полей — [`arch-00-glossary.md`](arch-00-glossary.md)):
|
||||
|
||||
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); изменения могут инициировать `contact.update` через триггеры.
|
||||
- **Поля профиля для UI:** master — последнее успешно синхронизированное значение; основной входящий поток на MVP — правки сотрудником в Bitrix24 (webhook → App DB).
|
||||
- **App → Bitrix:** триггеры `han_app` → `sync_queue` (`contact.update`).
|
||||
- **Bitrix → App:** webhook робота → `bitrix-sync`; запись с GUC `han.sync_suppress` (без эхо в очередь).
|
||||
- **Конфликт:** побеждает более позднее событие (`updated_at`, audit в `bitrix_sync`).
|
||||
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); только его фактическое изменение инициирует `contact.update`.
|
||||
- **ФИО, гражданство, email:** master — Битрикс24; App хранит последний успешно полученный snapshot для UI и не отправляет эти поля обратно.
|
||||
- **App → Bitrix:** триггеры `han_app` → `sync_queue` (`contact.map_or_create`, `contact.update`, `contact.deactivate`).
|
||||
- **Bitrix → App:** durable webhook inbox + reconciliation; запись с `SET LOCAL han.sync_suppress='true'` без эхо.
|
||||
- **Конфликт:** универсального правила «последнее событие побеждает» нет; применяется field mastership. Несовпадение identity/mapping создаёт business alert и не перезаписывает профиль.
|
||||
|
||||
## Аудит скачиваний
|
||||
|
||||
@@ -530,6 +557,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
||||
## Принципы безопасности
|
||||
|
||||
- Все защищенные пользовательские API требуют валидный JWT.
|
||||
- Компрометация одного сервиса не должна автоматически давать host root, Docker daemon, секреты или сетевой доступ соседних сервисов; требования к VM и production-деплою — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||||
- Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
|
||||
- Все внешние пользовательские соединения работают через HTTPS.
|
||||
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
|
||||
@@ -546,7 +574,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
||||
- Все публичные id создаются в формате UUID.
|
||||
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (канонический контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Service tokens (internal API)»; значения переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
||||
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
|
||||
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
|
||||
- Исходящие сообщения пользователя: internal `POST /internal/safety/v2/messages/check` → при `202` api-backend синхронно поллит `Location` до финального `200`/`403`, terminal failed `503` или timeout; public API не становится async.
|
||||
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
|
||||
- У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
|
||||
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
|
||||
@@ -559,13 +587,18 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
||||
|
||||
### Состав backend-контура
|
||||
|
||||
Минимальный целевой real-SMS контур на одной VM: `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector`. До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
|
||||
Минимальный целевой real-SMS контур разделён на два stack:
|
||||
|
||||
- ВМ1: `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1, `otel-collector`;
|
||||
- ВМ2: nginx с public webhook/private internal server blocks, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety, `otel-collector`.
|
||||
|
||||
До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
|
||||
|
||||
### Предлагаемая структура backend-репозитория
|
||||
|
||||
```text
|
||||
backend/
|
||||
docker-compose.yml # корневой compose: nginx + include сервисов + networks/volumes
|
||||
docker-compose.yml # root compose ВМ1
|
||||
.env.example
|
||||
nginx/
|
||||
docker-compose.yml
|
||||
@@ -579,12 +612,6 @@ backend/
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
message-safety/
|
||||
app/
|
||||
docker-compose.yml
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
bitrix-local-app/
|
||||
app/
|
||||
docker-compose.yml
|
||||
@@ -592,12 +619,6 @@ backend/
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
bitrix-sync/
|
||||
app/
|
||||
docker-compose.yml
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
keycloak/
|
||||
docker-compose.yml
|
||||
realm/
|
||||
@@ -611,14 +632,23 @@ backend/
|
||||
redis/
|
||||
docker-compose.yml
|
||||
observability/
|
||||
docker-compose.yml # сервис otel-collector
|
||||
docker-compose.yml # collector ВМ1
|
||||
otel-collector.yaml
|
||||
|
||||
processing/
|
||||
docker-compose.yml # root compose ВМ2
|
||||
nginx-internal/
|
||||
message-safety/
|
||||
bitrix-sync/
|
||||
clamav/
|
||||
redis/
|
||||
observability/ # collector ВМ2
|
||||
```
|
||||
|
||||
Детальная внутренняя структура каждого сервиса (`app/`, модули, миграции) определяется в профильных спецификациях модулей (TBD).
|
||||
|
||||
### Compose-контур
|
||||
### Compose-контуры
|
||||
|
||||
Корневой `backend/docker-compose.yml` подключает сервисные compose-файлы через `include`.
|
||||
`backend/docker-compose.yml` является единственным root Compose ВМ1; `processing/docker-compose.yml` — единственным root Compose ВМ2. Оба используют `include` и отдельные root-owned systemd units. Cross-host Docker network не используется.
|
||||
|
||||
Публикация портов наружу разрешена только `nginx` (`80/443`). Остальные сервисы доступны через Docker-сети и private VPC.
|
||||
На каждой VM host ports публикует только её nginx. ВМ1 публикует `80/443` своего application host. ВМ2 публикует `80/443` отдельного webhook host и private `8443`; public server block ВМ2 допускает только exact CRM webhook, private listener доступен только SG ВМ1/ops.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
| Переменная | Кто проверяет | Кто передаёт | Endpoint | Заголовок |
|
||||
|---|---|---|---|---|
|
||||
| `MESSAGE_SAFETY_SERVICE_TOKEN` | `message-safety` | `api-backend` | `POST/GET /internal/safety/v1/*` | `X-Service-Token` |
|
||||
| `MESSAGE_SAFETY_SERVICE_TOKEN` | `message-safety` | `api-backend` | `POST/GET /internal/safety/v2/*` | `X-Service-Token`, private TLS |
|
||||
| `BITRIX_INTERNAL_API_TOKEN` | `bitrix-local-app` | `api-backend` | `POST/GET /internal/openlines/v1/*` | `Authorization: Bearer` |
|
||||
| `BITRIX_LOCAL_APP_INTERNAL_TOKEN` | — | `api-backend` (исходящий) | то же | `Authorization: Bearer` |
|
||||
| `BITRIX_API_INBOX_TOKEN` | `api-backend` | `bitrix-local-app` | `POST /internal/openlines/v1/inbox` | `Authorization: Bearer` |
|
||||
@@ -47,7 +47,8 @@
|
||||
| Переменная | Назначение |
|
||||
|---|---|
|
||||
| `BITRIX_APPLICATION_TOKEN` | проверка событий Bitrix24 → `bitrix-local-app` `/bitrix/handler` |
|
||||
| `BITRIX_SYNC_WEBHOOK_TOKEN` | проверка webhook Bitrix24 → `bitrix-sync` `/bitrix/sync/webhook/contact` |
|
||||
| `BITRIX_SYNC_CONTACT_RECEIVER_TOKEN` | query `token` штатного HTTP-webhook робота Contact → receiver `bitrix-sync`; дополнительно source IP CIDR allow-list |
|
||||
| `BITRIX_SYNC_ALERT_RECEIVER_TOKEN` | query `token` штатного HTTP-webhook робота smart-process alert → receiver `bitrix-sync`; дополнительно source IP CIDR allow-list |
|
||||
|
||||
## Frontend ↔ api-backend
|
||||
|
||||
@@ -121,6 +122,8 @@
|
||||
| `503` | `dependency_unavailable` | Circuit open или недоступны safety/Bitrix/S3 | да |
|
||||
| `504` | `dependency_timeout` | Истёк timeout budget внешней зависимости | да |
|
||||
|
||||
`422 message_blocked` возвращает только стандартный public error envelope (`code`, generic `message`, `request_id`) без internal `rule_id`/`reason_code`. Пользовательский текст появляется отдельной локальной company-репликой из `text_resources` по мнемонике `safety.chat.blocked`.
|
||||
|
||||
Правило доступа к пользовательским ресурсам: для `dialog_id`, `message_id`, `attachment_id`, `document_id`, принадлежащих другому `user_id`, api-backend по умолчанию возвращает `404 not_found`, чтобы не раскрывать существование ресурса. `403 forbidden` используется только для операций, где сам факт ресурса уже известен пользователю или оператору.
|
||||
|
||||
### `POST /api/v1/auth/bootstrap` (после OTP)
|
||||
@@ -305,15 +308,17 @@ Post-MVP: допускается «текст + файлы» отдельной
|
||||
|
||||
Байты файла идут **напрямую в S3-quarantine** по короткоживущему **presigned URL**. `api-backend` не проксирует тело файла: выдаёт URL, проверяет результат, управляет lifecycle (promote/delete).
|
||||
|
||||
1. `POST .../attachments/init` (JWT) → `{ attachment_id, upload_url, upload_headers?, expires_at }` — `upload_url` = **presigned PUT** (или POST policy) в **S3-quarantine**; ключ объекта и ограничения (bucket, key prefix, `Content-Type`, max size) задаёт `api-backend`.
|
||||
1. `POST .../attachments/init` (JWT) → `{ attachment_id, upload_url, upload_headers, expires_at }` — **presigned PUT** в versioned S3-quarantine. Подпись обязательно включает `If-None-Match: *`, checksum header (`x-amz-checksum-sha256` либо подтверждённый эквивалент Selectel) и `Content-Type`; повторная запись того же key получает `412 Precondition Failed`.
|
||||
2. Frontend загружает байты **напрямую в Selectel S3** по `upload_url` (не через `api-backend`).
|
||||
3. `POST .../attachments/{attachment_id}/complete` с `checksum` (SHA-256) → api-backend проверяет наличие объекта в quarantine (HeadObject / размер / checksum), фиксирует metadata, `scan_status=pending`.
|
||||
3. `POST .../attachments/{attachment_id}/complete` с `checksum` (SHA-256) → api-backend получает authoritative `version_id`, ETag, size и server checksum; сравнивает client checksum и атомарно фиксирует `{quarantine_object_key, version_id, etag, checksum}`, `scan_status=pending`. Complete с другой версией/ETag/checksum → `409 resource_state_conflict`.
|
||||
4. `POST .../messages` с `attachment_id` + `checksum` → Message Safety.
|
||||
|
||||
Правила безопасности:
|
||||
|
||||
- у клиента **нет** постоянных S3 access keys — только одноразовый/короткий presigned URL;
|
||||
- presigned URL разрешает запись **только** в выделенный key в S3-quarantine (не в S3-data);
|
||||
- bucket versioning включён; Safety читает только сохранённый `version_id` с conditional ETag match;
|
||||
- allow-promote копирует именно эту version и использует conditional source ETag/checksum; mismatch запрещает delivery;
|
||||
- TTL URL короткий (константа модуля / `app_settings`, ориентир минуты);
|
||||
- скачивание из S3-data — отдельные **presigned GET** через `.../download-url` (с audit).
|
||||
|
||||
@@ -492,27 +497,32 @@ Frontend не обращается напрямую к Keycloak DB и не хр
|
||||
|
||||
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||
|---|---|---|---|---|
|
||||
| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | internal network + `X-Service-Token` |
|
||||
| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же `POST .../messages` | internal network + `X-Service-Token` |
|
||||
| `POST /internal/safety/v2/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | private HTTPS + internal CA + `X-Service-Token` |
|
||||
| `GET /internal/safety/v2/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же public `POST .../messages` | private HTTPS + internal CA + `X-Service-Token` |
|
||||
| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key |
|
||||
|
||||
HTTP-семантика от `message-safety`: `200 allow`, `403 deny`, `203 pending`.
|
||||
HTTP-семантика target v2 от `message-safety`: `200 allow`, `403 deny`, `202 Accepted/pending`. Текущие `/v1/*` и `203` относятся только к legacy stub и не являются production-контрактом.
|
||||
|
||||
Normative details v2: каждый verdict/pending содержит `processing_mode=standard|mock` и `config_version`; `202` обязательно содержит `Location`, `Retry-After`, `task_id`, `expires_at` и существует только в standard mode; terminal `503 task_failed` — `terminal=true,retryable=false`; transient `503 dependency_unavailable` — `terminal=false,retryable=true`; `409 safety_request_conflict` — non-retryable caller invariant. Все domain deny имеют `reason_code=message_blocked`.
|
||||
|
||||
Emergency MOCK включается только root-owned helper/restart на ВМ2. В MOCK нет content/link/file checks и `202`: `TEXT_FREE`/`FILE_FREE=true` → sync `200`, false → canonical sync `403`. Auth/DTO/idempotency/audit/rate limits сохраняются. Public API не раскрывает `processing_mode`.
|
||||
|
||||
Поведение `api-backend`:
|
||||
|
||||
1. Синхронно вызывает `POST .../check`, получает один из трёх кодов.
|
||||
2. При `200` / `403` — сразу завершает сценарий и отвечает клиенту.
|
||||
3. При `203` — **не ставит задачу в свою очередь анализа**; регулярно и синхронно поллит `GET .../tasks/{task_id}` до `200`/`403` или timeout (`MESSAGE_SAFETY_TASK_POLL_MAX_SEC`), затем отвечает клиенту.
|
||||
3. При `202` сохраняет `task_id`, `Location`, deadline и **не ставит задачу в свою очередь анализа**; синхронно поллит `Location`, соблюдая `Retry-After`, до `200`/`403`, terminal failed `503` или timeout.
|
||||
4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async).
|
||||
|
||||
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
|
||||
|
||||
Recovery contract для `han_app.safety_tasks`:
|
||||
|
||||
- запись создаётся, когда `message-safety` вернул `203 pending`, и содержит `task_id`, `message_id`, `attachment_id`, текущий `quarantine_object_key`, deadline и retry metadata;
|
||||
- если клиентское HTTP-соединение оборвалось или api-backend упал, recovery job продолжает poll `GET /internal/safety/v1/messages/tasks/{task_id}`;
|
||||
- запись создаётся, когда `message-safety` вернул `202 pending`, и содержит `task_id`, `Location`, `message_id`, `attachment_id`, текущие `quarantine_object_key/version_id/ETag`, deadline и retry metadata;
|
||||
- если клиентское HTTP-соединение оборвалось или api-backend упал, recovery job продолжает poll `GET /internal/safety/v2/messages/tasks/{task_id}`;
|
||||
- final allow выполняет idempotent promote quarantine → S3-data и продолжает delivery checkpoint в Open Lines;
|
||||
- final deny выполняет idempotent delete quarantine и выставляет `safety_status=blocked`, `delivery_status=rejected`;
|
||||
- при final deny создаётся локальная company-реплика с `text_resources.mnemonic=safety.chat.blocked`; она публикуется как `message.new`, но не отправляется в Open Lines;
|
||||
- timeout/circuit после recovery budget выставляет `delivery_status=failed`, оставляет audit trail и отдаёт объект на quarantine cleanup policy;
|
||||
- recovery job не принимает новых сообщений и не решает, sync или async нужна проверка: это остаётся ответственностью `message-safety`.
|
||||
|
||||
@@ -522,8 +532,12 @@ Recovery contract для `han_app.safety_tasks`:
|
||||
|---|---|---|---|
|
||||
| `200` / `allow` | `allowed` | `accepted` до вызова Open Lines; `delivered` только после успешной отправки в Open Lines | да |
|
||||
| `403` / `deny` | `blocked` | `rejected` | да |
|
||||
| `203` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
|
||||
| timeout / circuit open | `pending` или `blocked` по политике модуля | `failed` | да (ошибка инфраструктуры) |
|
||||
| `202` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
|
||||
| terminal failed `503`, `retryable=false` | `pending` | `failed` | да: public `503`, не deny |
|
||||
| `409 safety_request_conflict` | `pending` | `failed` | да: public `500` + alert, POST не повторять |
|
||||
| timeout / circuit open | `pending` | `failed` | да: public `503/504`, не deny |
|
||||
|
||||
Для mock file allow `MessageAttachment.scan_status=bypassed`; значение `clean` запрещено, так как фактической проверки не было. `Message.safety_processing_mode` и `Message.safety_config_version` хранятся для audit, но отсутствуют в public DTO.
|
||||
|
||||
Circuit breaker + timeout budget (I2): при открытом circuit на `message-safety` — не слать сообщение в Bitrix; вернуть клиенту безопасную ошибку зависимости.
|
||||
|
||||
@@ -580,7 +594,7 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes
|
||||
- если `api-backend` недоступен, `bitrix-local-app` хранит событие во внутреннем inbox, повторяет forward с exponential backoff и после исчерпания retry переводит запись в DLQ со статусом `dead_letter`;
|
||||
- `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery` только после успешного ответа `api-backend` или после идемпотентного duplicate-ack;
|
||||
- файлы оператора: api-backend скачивает по `download_url` (timeout budget) и сохраняет в **S3-data attachments** + `MessageAttachment`; в MVP применяются те же продуктовые лимиты `chat.attachments.allowed_*` и `chat.attachments.max_size_mb`, что и для клиентских файлов;
|
||||
- сообщения и файлы оператора считаются доверенным Bitrix24-channel для Message Safety: они не проходят outbound moderation pipeline, но проходят MIME/size validation, antivirus policy модуля и audit скачивания;
|
||||
- сообщения и файлы оператора считаются доверенным Bitrix24-channel: они **не** идут в quarantine и Message Safety, проходят только MIME/size validation и audit скачивания, затем сохраняются в S3-data. Остаточный malware-риск принят для MVP; UI/скачивание должны сохранять безопасный `Content-Disposition`/`Content-Type` и не исполнять active content.
|
||||
- пустой `text` и пустой `files` → reject события;
|
||||
- детальная JSON Schema — в `bitrix-local-app/openapi.yaml` и `api-backend/openapi.yaml`.
|
||||
|
||||
@@ -592,26 +606,31 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes
|
||||
|
||||
| Контракт | Тип | Владелец | Потребитель | Назначение |
|
||||
|---|---|---|---|---|
|
||||
| `han_app.sync_queue` | PostgreSQL | триггеры `han_app` (миграции App DB) | `bitrix-sync` | Асинхронная очередь App DB → Bitrix24: триггер ставит задачу при изменении отслеживаемых полей |
|
||||
| `han_app.sync_queue` | PostgreSQL | триггеры `han_app` (миграции App DB) | `bitrix-sync` | Durable очередь App DB → Bitrix24 с lease/fencing и active-only dedup |
|
||||
| `han.sync_suppress` (GUC) | PostgreSQL session | `bitrix-sync` | триггеры `han_app` | Подавление эхо-задач при записи данных от Bitrix24 в App DB |
|
||||
| `ClientProfile.bitrix_contact_id` | PostgreSQL | `bitrix-sync` | App DB | Маппинг профиля на CRM Contact после map/create |
|
||||
| `han_app.entity_external_mapping` | PostgreSQL | `bitrix-sync` | App DB | Универсальный маппинг App entity ↔ Bitrix entity (MVP: Contact) |
|
||||
| Обновление `sync_queue.status` | PostgreSQL | `bitrix-sync` | App DB | `processed` / `failed` / `dead_letter`, retry metadata |
|
||||
| `bitrix_sync.entity_external_mapping` | PostgreSQL | `bitrix-sync` | `bitrix-sync` | Единственная каноническая active/closed/broken история `user_id` ↔ Contact; App DB не хранит `b24_id` |
|
||||
| Обновление `sync_queue.status` | PostgreSQL | `bitrix-sync` | App DB | `pending/leased/retry_wait/processed/dead_letter/cancelled`, lease и safe error metadata |
|
||||
| `bitrix_sync.workflow_instances` / `crm_commands` | PostgreSQL | `bitrix-sync` | `bitrix-sync` | Persisted scenario state и конкретные Bitrix batch subcommands |
|
||||
| `bitrix_sync.webhook_inbox` | PostgreSQL | `bitrix-sync` | `bitrix-sync` | Durable приём, dedup и coalescing событий Битрикс24 |
|
||||
|
||||
Типы задач MVP (`sync_queue.task_type`):
|
||||
|
||||
- `contact.map_or_create` — матчинг/создание Contact, запись `bitrix_contact_id`, флаг регистрации в Bitrix24;
|
||||
- `contact.update` — push изменений профиля в Bitrix24.
|
||||
- `contact.map_or_create` — матчинг/создание Contact, запись mapping в schema `bitrix_sync`, флаг регистрации в Bitrix24;
|
||||
- `contact.update` — push только App-master телефона/служебных полей;
|
||||
- `contact.deactivate` — flag `N`, закрытие active mapping без удаления Contact.
|
||||
|
||||
`contact.rebind` не является задачей `han_app.sync_queue`: это audited административный workflow, создаваемый только через `bitrix_sync.request_bitrix_contact_rebind`.
|
||||
|
||||
`bitrix-sync` **не создаёт** `UserIdentity` / `ClientProfile` в auth-flow; вход worker — задачи из `sync_queue`, созданные триггерами.
|
||||
`entity_id` contact-задачи всегда равен `UserIdentity.id`; payload не содержит PII snapshot. Полный DDL/state-machine contract — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md), §§6–9.
|
||||
|
||||
### Internal HTTP `bitrix-sync` (ops, не hot path)
|
||||
|
||||
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||
|---|---|---|---|---|
|
||||
| `GET /internal/sync/v1/status` | `bitrix-sync` | ops / мониторинг | Глубина очереди, dead letter, последний успешный run | internal network + `BITRIX_SYNC_SERVICE_TOKEN` |
|
||||
| `GET /internal/sync/v1/status` | `bitrix-sync` | ops / мониторинг | Queue/workflow/webhook/reconciliation/limiter state без PII | internal network + `BITRIX_SYNC_SERVICE_TOKEN` |
|
||||
|
||||
Повтор dead letter и ручной replay в MVP — через БД/ops-процедуры; отдельный HTTP replay-endpoint — post-MVP.
|
||||
Публичный/manual replay HTTP endpoint отсутствует. Controlled ops-действия используют утверждённые процедуры с audit; произвольный `UPDATE` mapping запрещён.
|
||||
|
||||
## bitrix-local-app ↔ Bitrix24
|
||||
|
||||
@@ -629,14 +648,16 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes
|
||||
|
||||
| Контракт | Направление | Назначение |
|
||||
|---|---|---|
|
||||
| `crm.contact.get/list/add/update` | `bitrix-sync` → Bitrix24 | Поиск, создание и обновление Contact |
|
||||
| `POST /bitrix/sync/webhook/contact` | Bitrix24 (робот) → `bitrix-sync` | Исходящий webhook при изменении полей Contact, зарегистрированного в приложении |
|
||||
| PostgreSQL schema `bitrix_sync` | `bitrix-sync` ↔ PostgreSQL | Worker state, field mapping, retry/dead letter audit |
|
||||
| PostgreSQL schema `han_app` | `bitrix-sync` ↔ PostgreSQL | Очередь `sync_queue`, маппинг ID, обновление профиля (Bitrix → App) |
|
||||
| `crm.duplicate.findbycomm`, `crm.contact.get/add/update`, `crm.item.list`, `batch` | `bitrix-sync` → Bitrix24 | Первичный поиск, recovery, чтение, создание и точечное обновление Contact; reconciliation через `crm.item.list`, `entityTypeId=3`, `>=updatedTime`, `opened=1`, registration flag `=1` |
|
||||
| Contact receiver URL `/bitrix/sync/webhook/contact?token=...` | HTTP-webhook робот Битрикс24 → `bitrix-sync` | `application/x-www-form-urlencoded`, query token, source IP CIDR allow-list, durable inbox; затем snapshot по ID |
|
||||
| Alert receiver URL `/bitrix/sync/webhook/alert?token=...` | HTTP-webhook робот Битрикс24 → `bitrix-sync` | Form-urlencoded сигнал элемента smart process, query token и source IP CIDR allow-list |
|
||||
| Smart process «Конфликты синхронизации» | `bitrix-sync` ↔ Bitrix24 | Business alerts с fingerprint, occurrence и SLA |
|
||||
| PostgreSQL schema `bitrix_sync` | `bitrix-sync` ↔ PostgreSQL | Workflow/commands, inbox, snapshots, settings, alerts, reconciliation и technical DLQ |
|
||||
| PostgreSQL schema `han_app` | `bitrix-sync` ↔ PostgreSQL | Очередь `sync_queue` и обновление профиля (Bitrix → App); canonical mapping хранится только в `bitrix_sync` |
|
||||
|
||||
Очередь `han_app.sync_queue` и write-back — в разделе «api-backend ↔ bitrix-sync» выше.
|
||||
|
||||
`bitrix-sync` использует `BITRIX_SYNC_APP_DATABASE_URL` для `han_app` + `bitrix_sync`, только `BITRIX_SYNC_CRM_*` для Bitrix24 CRM REST и не читает OAuth-токены `bitrix-local-app`.
|
||||
`bitrix-sync` использует отдельный secret DB URL с search path/access к `bitrix_sync` и точечными GRANT на `han_app`, отдельный входящий webhook технического пользователя для CRM REST и отдельные application tokens исходящих webhook. OAuth-токены `bitrix-local-app` не читает.
|
||||
|
||||
## api-backend ↔ внешние хранилища
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# arch-03. Docker Compose blueprint
|
||||
|
||||
> Термины (имена бакетов S3, идентификаторы) — в [`arch-00-glossary.md`](arch-00-glossary.md). Контракт Message Safety Service — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md).
|
||||
> Термины (имена бакетов S3, идентификаторы) — в [`arch-00-glossary.md`](arch-00-glossary.md). Контракт Message Safety Service — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). VM/SSH, OS-роли, секреты, systemd-деплой и hardening — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||||
|
||||
## Назначение
|
||||
|
||||
@@ -8,31 +8,32 @@
|
||||
|
||||
Требования к безопасности на уровне приложения и данных — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md), раздел **«Принципы безопасности»**. Настоящий документ описывает только инфраструктурную реализацию этих принципов в compose/nginx: TLS, маршрутизация, сетевые границы, rate limits на edge. Значения переменных окружения — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Дублировать прикладные требования (JWT, валидация, CORS в API, PII в логах и т.п.) здесь не нужно — они остаются в `arch-01`.
|
||||
|
||||
## Единый compose-контур (обязательно)
|
||||
## Один root Compose project на каждую VM (обязательно)
|
||||
|
||||
Это зафиксированное архитектурное требование, а не рекомендация.
|
||||
|
||||
### Принцип единого входа
|
||||
### Принцип независимого входа по VM
|
||||
|
||||
- Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`).
|
||||
- Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть.
|
||||
- **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы:
|
||||
- ВМ1 и ВМ2 имеют по одному независимому root Compose project: `backend/docker-compose.yml` и `processing/docker-compose.yml`.
|
||||
- Каждый project поднимается своим root-owned systemd-unit/deployment helper. Пользователь `deploy` запускает только конкретные units и не получает доступ к Docker daemon; канон — arch-06.
|
||||
- Внутри одной VM её корневой `docker-compose.yml` — единственный источник правды. Cross-host Docker network и запуск одного Compose project на двух VM запрещены.
|
||||
- **Nginx ВМ1** является публичной точкой входа только своего контура:
|
||||
- `/api/*` → `api-backend` (REST и `WS /api/v1/realtime`; отдельный path `/realtime/*` **не** используется);
|
||||
- `/auth/*` → `keycloak`;
|
||||
- `/bitrix/*` (public: `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/*` для `bitrix-local-app`) → `bitrix-local-app`;
|
||||
- `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`;
|
||||
- exact `POST /callbacks/idgtl/sms` → `sms-service`; остальные методы и SMS paths не публикуются;
|
||||
- web-сборка frontend или прокси на dev-сервер;
|
||||
- `/internal/openlines/*`, `/internal/safety/*`, `/internal/sync/*`, `/internal/sms/*`, `/internal/notifications/*` **не публикуются** наружу — доступны только из внутренней Docker-сети.
|
||||
- Никакой другой `nginx` (ни в контейнере сервиса, ни на хосте) не терминирует внешний HTTPS для backend-контура. Site-конфиг `tohin.ru` на хосте, если используется, должен проксировать весь трафик на корневой `nginx` контейнера, а не на порты отдельных сервисов напрямую.
|
||||
- `/internal/openlines/*`, `/internal/safety/*`, `/internal/sync/*`, `/internal/sms/*`, `/internal/notifications/*` **не публикуются** наружу.
|
||||
- **Nginx ВМ2** независимо терминирует public HTTPS на отдельном host и публикует только exact Contact/alert webhook `bitrix-sync`. Отдельный private listener `8443` по internal CA маршрутизирует allow-listed Message Safety/internal paths.
|
||||
- Между public route ВМ1 и ВМ2 нет reverse-proxy chain или fallback. Каждый nginx имеет собственные DNS, сертификат, rate limits и release lifecycle.
|
||||
|
||||
### Структура compose через `include`
|
||||
### Структура Compose через `include`
|
||||
|
||||
Каждый сервис описывается в собственном `docker-compose.yml` внутри папки сервиса и подключается в корневой файл директивой `include`:
|
||||
Каждый сервис описывается в собственном `docker-compose.yml` и подключается в root-файл своей VM директивой `include`:
|
||||
|
||||
```text
|
||||
backend/
|
||||
docker-compose.yml # корневой: nginx + include сервисов + общие networks/volumes
|
||||
docker-compose.yml # root ВМ1
|
||||
.env
|
||||
nginx/
|
||||
docker-compose.yml # описание сервиса nginx (или секция в корневом)
|
||||
@@ -42,16 +43,21 @@ backend/
|
||||
.gitkeep
|
||||
api-backend/
|
||||
docker-compose.yml # описание сервиса api-backend
|
||||
message-safety/
|
||||
docker-compose.yml # описание сервиса message-safety
|
||||
bitrix-sync/
|
||||
docker-compose.yml # описание сервиса bitrix-sync
|
||||
bitrix-local-app/
|
||||
docker-compose.yml # описание сервиса bitrix-local-app
|
||||
keycloak/
|
||||
docker-compose.yml # описание сервиса keycloak (или секция в корневом)
|
||||
observability/
|
||||
docker-compose.yml # otel-collector и т.п.
|
||||
|
||||
processing/
|
||||
docker-compose.yml # root ВМ2
|
||||
nginx-internal/docker-compose.yml
|
||||
message-safety/docker-compose.yml
|
||||
bitrix-sync/docker-compose.yml
|
||||
clamav/docker-compose.yml
|
||||
redis/docker-compose.yml
|
||||
observability/docker-compose.yml
|
||||
```
|
||||
|
||||
Корневой `backend/docker-compose.yml` (принципиальная схема):
|
||||
@@ -62,8 +68,6 @@ name: han-chat
|
||||
include:
|
||||
- nginx/docker-compose.yml
|
||||
- api-backend/docker-compose.yml
|
||||
- message-safety/docker-compose.yml
|
||||
- bitrix-sync/docker-compose.yml
|
||||
- bitrix-local-app/docker-compose.yml
|
||||
- keycloak/docker-compose.yml
|
||||
- sms-service/docker-compose.yml
|
||||
@@ -81,14 +85,57 @@ volumes:
|
||||
nginx-certs:
|
||||
```
|
||||
|
||||
Root Compose ВМ2 включает собственный nginx с public/private server blocks, Message Safety API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety и локальный OTEL Collector. Секреты, сети и volumes двух projects не общие.
|
||||
|
||||
### Правила для сервисных compose-файлов
|
||||
|
||||
- Сервисный `docker-compose.yml` описывает **только** сервис(ы) своего модуля: образ, build context, `environment` (через `${VAR}` из корневого `.env`), порты (только внутренние, кроме случаев ниже), `depends_on`, healthcheck, подключение к сетям `public`/`backend`/`observability` (объявленным в корневом файле).
|
||||
- Сервисный `docker-compose.yml` описывает **только** сервис(ы) своего модуля: образ, build context, non-secret `environment` (через `${VAR}` из корневого `.env`), secret files/credentials, порты (только внутренние, кроме случаев ниже), `depends_on`, healthcheck, подключение к сетям `public`/`backend`/`observability` (объявленным в корневом файле).
|
||||
- Сервисный файл **не объявляет** сети и volumes верхнего уровня — они объявляются в корневом `docker-compose.yml`. Сервис только ссылается на них через `networks:` / `volumes:` (external-стиль не нужен, т.к. `include` объединяет файлы в один проект).
|
||||
- Публикация портов наружу (`ports:`) разрешена **только** для `nginx` (80/443). Все остальные сервисы используют `expose:` для внутренних портов и общаются через Docker-сети.
|
||||
- `bitrix-local-app` не публикует `8080` на хост (даже на `127.0.0.1`) — он доступен `api-backend` и `nginx` через сеть `backend`/`public`. Ранее применявшийся `127.0.0.1:8080:8080` считаем устаревшим; проверки через curl на `127.0.0.1:8080` заменяются на `docker compose exec bitrix-local-app` или прокси через `nginx`.
|
||||
- Каждый сервисный compose-файл должен запускаться и в составе корневого контура, и автономно (`docker compose -f bitrix-local-app/docker-compose.yml up`) для локальной разработки сервиса — при условии, что переменные окружения заданы. Для автономного запуска сервис может объявлять заглушки сетей/volumes, но в составе корневого контура они переопределяются общими.
|
||||
|
||||
### Обязательный container hardening
|
||||
|
||||
Для production-сервисов применяются требования [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md):
|
||||
|
||||
- непривилегированный `user`;
|
||||
- `security_opt: [no-new-privileges:true]`;
|
||||
- `cap_drop: [ALL]` с точечным возвратом документированных capabilities;
|
||||
- `read_only: true`, а writable paths — отдельные volume/tmpfs;
|
||||
- запрет `privileged`, host network/PID/IPC и Docker socket;
|
||||
- CPU/memory/PID limits, healthcheck и pinned image version/digest;
|
||||
- только необходимые Docker networks и read-only bind mounts.
|
||||
|
||||
Если сервису нужен root, writable root filesystem, capability или host mount, исключение фиксируется в профильной спецификации вместе с риском и компенсирующей мерой.
|
||||
|
||||
### Практическая валидация non-root/read-only image
|
||||
|
||||
Для каждого pinned digest Compose фиксирует и проверяет:
|
||||
|
||||
- фактические UID/GID основного процесса и entrypoint;
|
||||
- vendor entrypoint для non-root режима, если он отличается от root-варианта;
|
||||
- полный список writable paths: generated config, runtime/socket, cache/temp,
|
||||
logs и persistent state;
|
||||
- отдельный volume/tmpfs для каждого writable path с явными
|
||||
`uid/gid/mode`, размером и mount flags;
|
||||
- healthcheck именно того процесса, который реально запущен в контейнере;
|
||||
- restart semantics: успешный one-shot exit не должен превращаться в
|
||||
бесконечный restart/download loop.
|
||||
|
||||
Tmpfs скрывает ownership каталога из image, поэтому одного корректного
|
||||
`USER`/`chown` в Dockerfile недостаточно: ownership задаётся на самом tmpfs.
|
||||
Ошибка `read-only file system` исправляется добавлением минимального writable
|
||||
mount, а не `read_only: false`, root, `privileged` или broad capabilities.
|
||||
|
||||
Compose file secrets с bind-backed `file:` могут игнорировать декларативные
|
||||
`uid/gid/mode`. Их фактические host permissions создаёт secret materializer;
|
||||
rollout проверяет owner/mode из контейнера и с host, не полагаясь на YAML.
|
||||
|
||||
При сборке images необхоидмо добавлять нормализацию CRLF→LF
|
||||
(например, RUN sed -i 's/\r$//' <directory/service-name> \
|
||||
&& /bin/sh -n <directory/service-name>) и использовать проверку синтаксиса entrypoint
|
||||
|
||||
### Команды разработки
|
||||
|
||||
```text
|
||||
@@ -106,15 +153,25 @@ docker compose exec api-backend ruff format .
|
||||
|
||||
## Сервисы
|
||||
|
||||
### nginx
|
||||
### nginx ВМ1 и ВМ2
|
||||
|
||||
Reverse proxy и единственная публичная точка входа в Docker Compose контур.
|
||||
На каждой VM работает собственный nginx в независимом root Compose. ВМ1 обслуживает frontend/API/auth/Open Lines/SMS; ВМ2 напрямую принимает CRM webhook и отдельно предоставляет private Message Safety ingress. Публичный трафик ВМ2 не проксируется через ВМ1.
|
||||
|
||||
Требования:
|
||||
|
||||
- публикует наружу только `80` и `443` (см. политику HTTP ниже);
|
||||
- принимает внешний HTTPS-трафик;
|
||||
- выполняет TLS termination на reverse proxy; внутренний HTTP между контейнерами — только в закрытой Docker-сети `backend`;
|
||||
- non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`,
|
||||
`/var/cache/nginx`, `/var/run` и `/tmp`; tmpfs задаёт явные UID/GID/mode и
|
||||
`nofile` согласован с `worker_connections`;
|
||||
- если image entrypoint выполняет `envsubst`, output directory обязан быть
|
||||
writable целевому UID, а основной `nginx.conf` подключает конкретный
|
||||
generated file, чтобы отсутствующий результат не дал ложный успешный
|
||||
`nginx -t` через wildcard include;
|
||||
- pre-start config test выполняет реальный image entrypoint. До запуска
|
||||
upstream-контейнеров их host variables временно подменяются loopback IP
|
||||
только в test container; production Compose сохраняет service DNS names;
|
||||
- **политика HTTP/HTTPS по доменам** (каноническое правило — [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности»):
|
||||
- **веб-домен** (frontend, SPA, статика): `listen 80` допускается **только** для безусловного редиректа `301`/`308` на HTTPS; обработка бизнес-логики по HTTP запрещена;
|
||||
- **API-домен** (если выделен отдельный host, напр. `api.example.ru`): **не** слушает порт `80`; только `listen 443 ssl`; HTTP-запросы к API-домену недоступны;
|
||||
@@ -124,7 +181,7 @@ Reverse proxy и единственная публичная точка вход
|
||||
- маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
|
||||
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
|
||||
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
|
||||
- маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
|
||||
- nginx ВМ2 маршрутизирует только exact `/bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert` в локальный `bitrix-sync`;
|
||||
- маршрутизирует только exact `POST /callbacks/idgtl/sms` в `sms-service:8080`; применяет HTTPS, подтверждённый allowlist source IP Direct, body/rate limits и redaction Basic Authorization;
|
||||
- закрывает `/internal/*` (в т.ч. `bitrix-local-app`, `message-safety`, `bitrix-sync` ops) от публичного доступа — только private network Docker/VPC;
|
||||
- **не публикует** `message-safety` наружу;
|
||||
@@ -149,7 +206,7 @@ Python FastAPI backend.
|
||||
Требования:
|
||||
|
||||
- запускается после доступности managed PostgreSQL, `keycloak`, `redis`;
|
||||
- применяет настройки из `.env`;
|
||||
- применяет non-secret настройки из `.env` и runtime secrets из явно смонтированных secret files;
|
||||
- отдает `/health/live` и `/health/ready`;
|
||||
- корректно работает за reverse proxy и доверяет proxy headers только от `nginx`;
|
||||
- применяет API-level rate limits с состоянием в Redis;
|
||||
@@ -165,38 +222,63 @@ Python FastAPI backend.
|
||||
|
||||
### message-safety
|
||||
|
||||
Отдельный backend-сервис проверки входящих сообщений пользователя. HTTP-контракт — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety».
|
||||
Отдельный сервис ВМ2 для проверки исходящих сообщений пользователя. HTTP-контракт — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety».
|
||||
|
||||
Требования:
|
||||
|
||||
- запускается после доступности managed PostgreSQL (схема `message_safety`), `redis`;
|
||||
- **не публикуется** через `nginx` — доступен только из внутренней Docker-сети;
|
||||
- отдаёт `/health/live` и `/health/ready` (ready проверяет PostgreSQL, Redis, workers, read-доступ к S3-quarantine);
|
||||
- exposing endpoints: `POST /internal/safety/v1/messages/check`, `GET /internal/safety/v1/messages/tasks/{task_id}` (internal Docker network + `X-Service-Token` / `MESSAGE_SAFETY_SERVICE_TOKEN`);
|
||||
- запускается после доступности managed PostgreSQL (схема `message_safety`) и Redis Safety;
|
||||
- сам Message Safety наружу не публикуется; api-backend обращается через private listener nginx ВМ2 по HTTPS;
|
||||
- отдаёт `/health/live` и capability-aware `/health/ready`: PostgreSQL/config — core gate, Redis/workers/S3/DNS влияют на отдельные capabilities; в MOCK normal capabilities показываются как `bypassed`, mode — `degraded`;
|
||||
- target endpoints: `POST /internal/safety/v2/messages/check`, `GET /internal/safety/v2/messages/tasks/{task_id}` (private HTTPS + `X-Service-Token`);
|
||||
- read-only доступ к S3-quarantine (отдельный access key без прав записи);
|
||||
- использует отдельную схему `message_safety` в managed PostgreSQL и отдельный DB-user;
|
||||
- использует Redis (отдельная DB, напр. `redis://redis:6379/2`) для verdict cache и rate limits;
|
||||
- runtime role читает immutable active `message_safety.config_versions`; создавать/активировать config может только отдельный migration/config-admin job;
|
||||
- использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
|
||||
- запускает async workers для file scan из S3-quarantine;
|
||||
- API container получает read-only root-owned `/etc/han-chat/message-safety-mode.env`; менять его и перезапускать stack может только fixed helper, разрешённый `deploy` через exact-argument sudoers;
|
||||
- экспортирует traces/logs в `otel-collector`;
|
||||
- таймауты: POST check 5 s, GET task 2 s, file scan 60 s (см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), переменные `MESSAGE_SAFETY_*`).
|
||||
|
||||
### ClamAV на ВМ2
|
||||
|
||||
`clamd` и `freshclam` используют один immutable image digest, но разные
|
||||
security-профили:
|
||||
|
||||
- оба запускаются через vendor `init-unprivileged`, а не root entrypoint;
|
||||
- `clamd` читает volume signatures read-only, не подключён к signature CDN и
|
||||
имеет healthcheck daemon socket;
|
||||
- `freshclam` один пишет в signatures и имеет только разрешённый egress к CDN;
|
||||
- `/run/clamav` — отдельный runtime volume, `/var/log/clamav` и `/tmp` —
|
||||
ограниченные tmpfs с UID/GID ClamAV;
|
||||
- `freshclam` работает как foreground daemon с заданным interval; inherited
|
||||
healthcheck `clamd` отключён, потому что updater не поднимает daemon socket;
|
||||
- работоспособность updater подтверждается состоянием `Up`, отсутствием
|
||||
restart loop и отдельным контролем возраста/signature version, а не
|
||||
искусственным container healthcheck.
|
||||
|
||||
Смена digest ClamAV требует повторной проверки entrypoint, UID/GID, writable
|
||||
paths, `clamd` health и фактического обновления signatures. Нельзя менять
|
||||
только tag/digest, считая security contract image неизменным.
|
||||
|
||||
### bitrix-sync
|
||||
|
||||
Python worker/service **двусторонней** синхронизации App DB ↔ Bitrix24 CRM.
|
||||
Python API/worker service ВМ2 для durable двусторонней синхронизации App DB ↔ Bitrix24 CRM. Каноническая постановка — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md).
|
||||
|
||||
Требования:
|
||||
|
||||
- запускается после готовности managed PostgreSQL, `redis`;
|
||||
- запускается при доступном managed PostgreSQL; Redis не является зависимостью sync;
|
||||
- читает задачи из `han_app.sync_queue` (заполняется триггерами App DB);
|
||||
- имеет прямой доступ к `han_app` (`BITRIX_SYNC_APP_DATABASE_URL`) и схеме `bitrix_sync`;
|
||||
- выполняет map/create Contact по телефону (интервал `BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC`, default 60);
|
||||
- push обновлений Contact (интервал `BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC`, default 30);
|
||||
- принимает webhook `POST /bitrix/sync/webhook/contact` от роботов Bitrix24;
|
||||
- при записи в App DB от Bitrix использует GUC `han.sync_suppress=true`;
|
||||
- поддерживает graceful shutdown и rate limiting Bitrix REST;
|
||||
- владеет схемой `bitrix_sync` и имеет только точечные GRANT на queue/profile/mapping в `han_app`;
|
||||
- выполняет durable workflows `contact.map_or_create`, `contact.update`, `contact.deactivate` и административный `contact.rebind`;
|
||||
- принимает `POST /bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert`, durable сохраняет до `2xx`;
|
||||
- выполняет Contact/alert reconciliation на случай потери обычного webhook;
|
||||
- при записи в App DB от Bitrix использует `SET LOCAL han.sync_suppress='true'`;
|
||||
- использует Bitrix `batch`, общий token bucket и bounded in-flight; default 2 HTTP requests/sec;
|
||||
- поддерживает leases/fencing, graceful shutdown, retry до 24 часов, technical DLQ и business alerts;
|
||||
- не блокирует пользовательский API при ошибках Битрикс24;
|
||||
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
|
||||
- **не участвует** в hot path чата Open Lines;
|
||||
- принимает публичный CRM webhook после TLS termination/rate limit на собственном nginx ВМ2; ВМ1 в route не участвует;
|
||||
- включается/отключается флагом **`BITRIX_SYNC_ENABLED`** в `.env` (default `true`): при `false` сервис стартует в no-op/degraded режиме, но не обрабатывает `sync_queue` и не выполняет синхронизацию с Bitrix24 CRM.
|
||||
|
||||
### bitrix-local-app
|
||||
@@ -224,9 +306,16 @@ Python worker/service **двусторонней** синхронизации Ap
|
||||
|
||||
- подключение только из приватной сети VPC (VM → managed PostgreSQL);
|
||||
- одна managed база: схемы `han_app`, `bitrix_sync`, `message_safety`, `bitrix_local`, `keycloak`, `sms`;
|
||||
- отдельные DB-пользователи с доступом только к своей схеме; исключение: `bitrix_sync_user` дополнительно имеет ограниченный GRANT на `han_app` (`sync_queue`, `entity_external_mapping`, tracked columns профиля — детали схемы TBD в спецификации database);
|
||||
- отдельные DB-пользователи с доступом только к своей схеме; исключение: `bitrix_sync_user` дополнительно имеет column/table GRANT на `han_app.sync_queue`, чтение необходимых identity/profile columns и controlled update CRM-master profile fields согласно module-07 §13; mapping/rebind находятся в собственной schema `bitrix_sync`;
|
||||
- TLS к managed PostgreSQL обязателен;
|
||||
- миграции Alembic выполняются отдельной командой при деплое;
|
||||
- миграции Alembic выполняются отдельным controlled job с migration URL,
|
||||
который не попадает в runtime services;
|
||||
- временные cross-schema `USAGE`/`SELECT` выдаёт owner/DB administrator на
|
||||
конкретные объекты до migration gate и отзывает после успешного commit;
|
||||
migration чужой схемы не выполняет `REVOKE`/`ALTER` её объектов;
|
||||
- release image сохраняет все уже использованные Alembic revision IDs, в том
|
||||
числе legacy/no-op baseline, чтобы `upgrade head` не требовал ручного
|
||||
`stamp` production DB;
|
||||
- бэкапы и PITR — на стороне провайдера.
|
||||
|
||||
### keycloak
|
||||
@@ -265,7 +354,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
||||
- хранить счетчики API-level rate limits и idempotency keys (`api-backend`);
|
||||
- поддерживать TTL для лимитных и idempotency ключей;
|
||||
- **не** хранить OTP counters для `api-backend` (OTP — зона Keycloak/SPI);
|
||||
- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization;
|
||||
- sync_queue, leases, limiter coordination и durable wake-up fallback хранятся в PostgreSQL; `LISTEN/NOTIFY` — только optimization, Redis sync-service не использует;
|
||||
- разделение DB index (I4): см. arch-04 (`REDIS_URL`, `MESSAGE_SAFETY_REDIS_URL`).
|
||||
|
||||
### otel-collector
|
||||
@@ -282,27 +371,39 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
||||
|
||||
Рекомендуемые сети:
|
||||
|
||||
- `public`: `nginx`, `keycloak` (для прокси `/auth/*`), frontend static/dev access, внешний HTTPS entrypoint.
|
||||
- `backend`: `api-backend`, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `keycloak`, `redis` (managed PostgreSQL — вне compose, в VPC).
|
||||
- `egress`: только сервисы с утверждёнными исходящими интеграциями; `sms-worker` обращается к Direct, Keycloak — только к `smartcaptcha.cloud.yandex.ru` для server-side validation. Production real mode требует фактический статический egress IP/NAT, записанный в inventory и переданный Direct для allowlist.
|
||||
- `observability`: `otel-collector` + сервисы, экспортирующие telemetry.
|
||||
- ВМ1 `public`: edge nginx, Keycloak proxy и frontend entrypoint.
|
||||
- ВМ1 `backend`: `api-backend`, `bitrix-local-app`, Keycloak, SMS API и Redis DB0/DB1.
|
||||
- ВМ2 `backend`: nginx, Safety API/worker, `bitrix-sync`, `clamd` и Redis Safety.
|
||||
- `egress` подключается только к процессам с назначением: `freshclam` → signature CDN; `bitrix-sync` → утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/clamd/Redis нет.
|
||||
- `observability` существует отдельно на каждой VM и ведёт в её local collector.
|
||||
|
||||
Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS.
|
||||
Базы данных, Redis, OTLP receivers и internal service ports не публикуются. Cross-host calls идут через private network, точные SG и TLS.
|
||||
|
||||
## Volumes
|
||||
|
||||
Минимальные volumes (production на одной VM):
|
||||
Минимальные volumes:
|
||||
|
||||
- `redis-data` (опционально, если нужна персистентность);
|
||||
- certbot / TLS volumes для `nginx`.
|
||||
- ВМ1: Redis DB0/DB1 data, public TLS/ACME, local OTEL queue;
|
||||
- ВМ2: Redis Safety data (rebuildable), ClamAV signatures и runtime,
|
||||
internal TLS secrets, local OTEL queue.
|
||||
|
||||
Public TLS и ACME на ВМ2 — не named volumes: Compose монтирует read-only host
|
||||
staging `/var/lib/han-chat/public-tls` и ACME webroot
|
||||
`/var/lib/han-chat/acme`. Internal TLS certificate/key передаются отдельными
|
||||
Compose secrets и не объединяются с public TLS.
|
||||
|
||||
Данные PostgreSQL **не** хранятся в Docker volumes — только managed PostgreSQL вне compose.
|
||||
|
||||
Persistent volume используется только для state, переживающего recreate.
|
||||
Generated config, PID/socket, cache и logs без retention размещаются в
|
||||
ограниченных tmpfs. Один writable volume не объединяет config/executable со
|
||||
state.
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Корневой `backend/.env` читается всеми сервисами compose через `${VAR}` в сервисных `docker-compose.yml`. Канонический `.env.example` и `app_settings` — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md); контракты service tokens — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||
Каждая VM имеет свой allow-listed non-secret env manifest. Секреты доставляются отдельными service files согласно arch-04/06; общий env/secret bundle двух VM запрещён.
|
||||
|
||||
Для smoke-продюсера обязателен `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; это secret, а не `app_settings`. Compose передаёт его только `api-backend` и notification workers. Зарегистрированные entrypoints: `han-notification-expire-worker` и `han-notification-draft-cleanup-worker`; выдуманный command без project script в deployment запрещён.
|
||||
Для smoke-продюсера обязателен `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; это runtime secret, а не `.env`/`app_settings`. Compose монтирует его только `api-backend` и notification workers. Зарегистрированные entrypoints: `han-notification-expire-worker` и `han-notification-draft-cleanup-worker`; выдуманный command без project script в deployment запрещён.
|
||||
|
||||
## HTTPS и TLS
|
||||
|
||||
@@ -337,6 +438,13 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
||||
- инструкция по установке всегда открывается новой вкладкой, поэтому CSP SPA задаёт `frame-src 'none'`; allow-list iframe для инструкций отсутствует;
|
||||
- секретный ключ сертификата не коммитится в репозиторий;
|
||||
- использовать сертификаты доверенного CA; автоматизировать выпуск и продление (Let's Encrypt + reload `nginx`);
|
||||
- non-root nginx не монтирует root-only дерево Let's Encrypt целиком:
|
||||
root deploy hook атомарно копирует только `fullchain.pem` и `privkey.pem` в
|
||||
host staging `root:<dedicated-tls-group>` (`0750`, файлы `0640`), а Compose
|
||||
монтирует staging read-only;
|
||||
- reload после renewal выполняется только после `openssl` certificate/key
|
||||
match и полного `nginx -t`; internal TLS PEM также проверяется на raw PEM,
|
||||
отсутствие literal `\n`/double-base64 и совпадение ключа;
|
||||
- закрыть прямой доступ к внутренним портам контейнеров извне.
|
||||
|
||||
## Nginx routing для Bitrix24 Local App
|
||||
@@ -356,13 +464,17 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
||||
- для `/bitrix/*` callbacks кэширование отключено;
|
||||
- для `/bitrix/*` callbacks включены отдельные rate limits, но они не должны блокировать легитимные webhook-повторы Bitrix24.
|
||||
|
||||
## Nginx routing для bitrix-sync (CRM webhook)
|
||||
## Nginx routing для bitrix-sync (CRM webhooks)
|
||||
|
||||
`nginx` маршрутизирует публичные webhook CRM sync в `bitrix-sync`:
|
||||
Публичный nginx ВМ2 маршрутизирует только два exact webhook CRM sync в локальный `bitrix-sync`:
|
||||
|
||||
- `POST /bitrix/sync/webhook/contact` — исходящий webhook от роботов Bitrix24 при изменении Contact;
|
||||
- проверка `BITRIX_SYNC_WEBHOOK_TOKEN` выполняется в `bitrix-sync`;
|
||||
- кэширование отключено; rate limits не должны блокировать легитимные повторы Bitrix24;
|
||||
- `POST /bitrix/sync/webhook/contact` — изменение Contact;
|
||||
- `POST /bitrix/sync/webhook/alert` — изменение элемента smart process конфликтов;
|
||||
- nginx до proxy проверяет source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; автоматическое расширение allow-list запрещено;
|
||||
- штатный робот передаёт отдельный Contact/alert receiver token в query и form-urlencoded document/auth fields; token, query и body не попадают в logs/traces;
|
||||
- document/entity/domain/member fields проверяются в `bitrix-sync`; local app/event handler для CRM sync не используется;
|
||||
- кэширование отключено; source IP/body/method/rate limits применяются до private proxy; IP rejects экспортируются в telemetry без IP label;
|
||||
- при sync disabled/cutover route закрыт либо возвращает retryable `503`, а не `202 ignored`;
|
||||
- `/internal/sync/v1/*` не публикуется наружу (только internal network + `BITRIX_SYNC_SERVICE_TOKEN`).
|
||||
|
||||
## Rate limits и защита от abuse
|
||||
@@ -420,9 +532,9 @@ WAF не заменяет обязательные лимиты, валидац
|
||||
Минимальные проверки:
|
||||
|
||||
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
|
||||
- `api-backend`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL `han_app`, Redis `/0` и `/1`, доступность JWKS/discovery Keycloak, S3 permissions для presign/promote и readiness `message-safety`;
|
||||
- `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine);
|
||||
- `bitrix-sync`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL, доступ к `sync_queue`, worker state и CRM webhook config; при `BITRIX_SYNC_ENABLED=false` ready возвращает degraded/not-ready с причиной `sync_disabled`;
|
||||
- `api-backend`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL `han_app`, Redis DB0/DB1, JWKS/discovery Keycloak и S3 permissions. Недоступность remote Message Safety отражается как degraded dependency и блокирует только send path, но не readiness read API;
|
||||
- `message-safety`: `/health/ready` возвращает process/core status и capability map `text|links|files|worker`; ClamAV/S3 не выключают text, DNS не выключает text без ссылок, Redis hot cache не является core gate;
|
||||
- `bitrix-sync`: `/health/live` проверяет процесс; `/health/ready` проверяет validated config/secrets, PostgreSQL/grants, worker/limiter state и CRM webhook config; invalid credential/config даёт not-ready, краткая CRM outage — degraded по stale policy; при `BITRIX_SYNC_ENABLED=false` ready возвращает not-ready `sync_disabled`;
|
||||
- `bitrix-local-app`: `/health/live` проверяет процесс; `/health/ready` показывает PostgreSQL, OAuth-токены после установки приложения, connector activation и возможность forward в API при включённом `BITRIX_API_FORWARD_URL`;
|
||||
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
|
||||
- `sms-service`: live — процесс; ready — schema/migrations, active approved template, sender/API key; Direct доступность — отдельный dependency status, не причина restart loop;
|
||||
@@ -430,31 +542,63 @@ WAF не заменяет обязательные лимиты, валидац
|
||||
|
||||
Наружу через `nginx` публикуются только health endpoint, которые нужны Bitrix24 install/callback validation или внешнему мониторингу. Internal services (`message-safety`, internal `bitrix-sync`, Redis, otel) проверяются только из Docker/VPC-сети.
|
||||
|
||||
Healthcheck не копируется между разными commands одного image без проверки.
|
||||
Если updater не запускает daemon, daemon-socket healthcheck для него
|
||||
отключается. Freshness данных контролируется отдельной метрикой/проверкой
|
||||
timestamp и версии, а `restart: unless-stopped` применяется только к
|
||||
долгоживущему foreground process.
|
||||
|
||||
## Порядок запуска
|
||||
|
||||
1. `redis` (managed PostgreSQL должна быть доступна до старта зависимых сервисов).
|
||||
2. `otel-collector`.
|
||||
3. `api-backend` и seed OTP settings.
|
||||
4. `sms-service`/worker после migrations/seed (Keycloak пока mock).
|
||||
5. `keycloak`.
|
||||
6. `message-safety`.
|
||||
7. `bitrix-local-app`.
|
||||
8. `bitrix-sync`.
|
||||
9. Notification expire/cleanup workers после готовности `api-backend` и регистрации их entrypoints.
|
||||
10. `nginx`.
|
||||
Общие prerequisites: managed PostgreSQL доступна из private network,
|
||||
host-side secrets/TLS materialized, controlled migrations и seed завершены.
|
||||
|
||||
ВМ2 запускается в порядке:
|
||||
|
||||
1. `redis-safety` и local `otel-collector`;
|
||||
2. `freshclam`, затем `clamd` до состояния healthy;
|
||||
3. Message Safety API/worker и `bitrix-sync`;
|
||||
4. nginx — последним, после успешного config test;
|
||||
5. private HTTPS ВМ1→ВМ2 и capability health проверяются до cutover.
|
||||
|
||||
ВМ1 запускается в порядке:
|
||||
|
||||
1. Redis и `otel-collector`;
|
||||
2. Keycloak, `sms-service`/worker, `api-backend` и `bitrix-local-app` с их
|
||||
readiness-зависимостями;
|
||||
3. notification expire/cleanup workers после готовности `api-backend`;
|
||||
4. nginx — последним.
|
||||
|
||||
Порядок rollout SMS подробнее задаёт module-11/module-10. Зависимости запуска не образуют цикл: Keycloak стартует при недоступном `sms-service`; это блокирует только новые real-mode orders, а verify уже active challenges продолжается по snapshot.
|
||||
|
||||
`depends_on` не заменяет проверку готовности. Сервисы должны уметь ждать зависимости или корректно завершаться с понятной ошибкой. `api-backend` должен ждать готовности `message-safety` (healthcheck), т.к. отправка сообщения синхронно зависит от `POST /internal/safety/v1/messages/check`.
|
||||
`depends_on` не заменяет проверку готовности. Сервисы должны уметь ждать зависимости или корректно деградировать. `api-backend` может стать ready для read API до ВМ2, но send endpoint обязан fail-closed при недоступной требуемой capability Safety.
|
||||
|
||||
## Развёртывание на одной VM
|
||||
## Развёртывание на ВМ1 и ВМ2
|
||||
|
||||
Production-контур на `tohin.ru`:
|
||||
1. ВМ1, ВМ2, SigNoz и managed PostgreSQL находятся в одной private network/VPC; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress.
|
||||
2. Managed PostgreSQL не имеет public IP; SG разрешает каждой VM только нужные DB roles/schemas.
|
||||
3. На каждой VM отдельный root-owned systemd unit выполняет её root Compose; `deploy` не входит в `docker`.
|
||||
4. Public nginx ВМ2 публикует `80/443`; `80` используется только для ACME/redirect, `443` — только exact CRM webhook. Private `8443` разрешён только от SG ВМ1 и ops для Message Safety/internal access.
|
||||
5. Deploy/cutover ВМ2 не требует изменения public routes ВМ1. Для `bitrix-sync` rollback закрывает webhook routes на nginx ВМ2 либо возвращает retryable `503`, останавливает claims и сохраняет durable tasks/mapping; возврат к фиктивному `202 ignored` запрещён.
|
||||
|
||||
1. VM и managed PostgreSQL в одном VPC/кластере провайдера.
|
||||
2. Managed PostgreSQL без публичного IP; security group разрешает подключение только с VM.
|
||||
3. `docker compose up -d` на VM поднимает все сервисы кроме БД.
|
||||
4. Сервисы подключаются к managed PostgreSQL по приватному FQDN/IP.
|
||||
Перед первым `up` и после смены любого image digest обязательны permission
|
||||
gates:
|
||||
|
||||
1. проверить LF/shebang и root ownership release-артефактов;
|
||||
2. сверить UID/GID контейнеров с owner/mode host bind mounts, secret files и
|
||||
TLS staging;
|
||||
3. проверить доступ целевого UID и отказ постороннему UID;
|
||||
4. выполнить PEM parse/key-match и Compose render;
|
||||
5. запустить image-native config test/entrypoint с production hardening и
|
||||
временными test-only upstream values;
|
||||
6. выдать migration-role только необходимые временные cross-schema grants,
|
||||
выполнить Alembic и отозвать grants владельцем;
|
||||
7. после старта проверить отсутствие permission/restart loops, реальные
|
||||
healthchecks и freshness updater data.
|
||||
|
||||
Неуспех gate исправляется в ownership, mount/entrypoint contract или DB grants.
|
||||
Временное ослабление `read_only`, запуск root, broad chmod, добавление в
|
||||
`docker` group и расширение DB privileges запрещены.
|
||||
|
||||
## Production-замечания
|
||||
|
||||
@@ -467,15 +611,15 @@ Production-контур на `tohin.ru`:
|
||||
|
||||
Переменные — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), блок «Frontend (nginx)».
|
||||
|
||||
Docker Compose на одной VM — production-контур первого этапа. Позже при росте нагрузки можно отдельно решить:
|
||||
Два root Compose projects — production-контур первого этапа. Позже при росте можно отдельно решить:
|
||||
|
||||
- вынос Redis в managed cache;
|
||||
- managed object storage;
|
||||
- secret manager;
|
||||
- TLS, reverse proxy или managed ingress;
|
||||
- backup и restore;
|
||||
- централизованный мониторинг;
|
||||
- горизонтальное масштабирование API и worker.
|
||||
- перенос `bitrix-sync` на ВМ3;
|
||||
- горизонтальное масштабирование Safety API/worker/scan lanes;
|
||||
- managed internal load balancer/mTLS;
|
||||
- HA ВМ2.
|
||||
|
||||
Secret manager не является будущей опцией: для VM с утверждённым egress действует `han-secrets` + Selectel Secrets Manager, а для private/no-egress VM — контролируемая доставка root-owned secret files без сетевого secret-agent. Модель зафиксирована в arch-04 и arch-06.
|
||||
|
||||
### Backup, restore и cleanup
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# arch-04. Настройки и изменяемые параметры
|
||||
|
||||
> **`.env`** — инфраструктура и секреты. **`app_settings`** (App DB) — единственный источник бизнес-настроек. Имена полей и enum — [`arch-00-glossary.md`](arch-00-glossary.md). Docker Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
|
||||
> **`.env`** — только несекретная инфраструктурная конфигурация. Production-секреты доставляются отдельно по [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). **`app_settings`** (App DB) — единственный источник бизнес-настроек. Имена полей и enum — [`arch-00-glossary.md`](arch-00-glossary.md). Docker Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
|
||||
|
||||
## Цель
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
| Слой | Где | Что |
|
||||
|---|---|---|
|
||||
| **Инфраструктура** | `.env` | подключения, URL, секреты, nginx/TLS, service tokens |
|
||||
| **Инфраструктура** | `.env` | несекретные host/port/URL, nginx/TLS, режимы и технические параметры |
|
||||
| **Production-секреты** | Selectel Secrets Manager → `/run/han-chat/secrets`; для no-egress VM — root-owned files | credential-bearing DSN, пароли, private keys, service/webhook tokens, provider credentials |
|
||||
| **Бизнес-логика** | таблица **`app_settings`** | лимиты, флаги, телефоны, типы файлов, CORS, consent URLs |
|
||||
| **Настройки SMS runtime** | таблица **`sms.sms_setting`** | sender default, provider timeouts, callback flag, worker intervals |
|
||||
| **Контент** | `text_resources`, `popular_questions` | тексты UI |
|
||||
@@ -17,22 +18,31 @@ Managed PostgreSQL **поднимается до** развёртывания п
|
||||
|
||||
## Источники настроек
|
||||
|
||||
### `.env` — только инфраструктура
|
||||
### `.env` — только несекретная инфраструктура
|
||||
|
||||
Корневой `backend/.env` читается сервисами compose. В репозитории — `.env.example`, не `.env`.
|
||||
|
||||
**Допустимо в `.env`:**
|
||||
|
||||
- URL сервисов, публичные endpoint, порты;
|
||||
- строки подключения PostgreSQL, Redis, Keycloak DB;
|
||||
- секреты: S3, Bitrix OAuth, service tokens, webhook-тokens;
|
||||
- host/port/database/schema без паролей и токенов;
|
||||
- параметры **nginx/TLS** и edge rate limits (`NGINX_RATE_LIMIT_*`);
|
||||
- идентификация Keycloak: realm, audience, public/internal URL;
|
||||
- переключатель и секрет временного OTP mock (`KEYCLOAK_OTP_MOCK_*`); mock обязателен до прохождения real-SMS rollout gates и запрещён как незаявленный fallback;
|
||||
- переключатель и client/server keys Yandex SmartCaptcha (`KEYCLOAK_YANDEX_CAPTCHA_*`); сложность остаётся в Yandex Cloud, а server key не попадает в тему/логи;
|
||||
- переключатели OTP mock и Yandex SmartCaptcha, а также публичный CAPTCHA client key; mock code и CAPTCHA server key являются секретами;
|
||||
- `SECRETS_SOURCE=selectel|file`, который выбирает утверждённый механизм доставки, но не содержит secret value;
|
||||
- технические параметры сервисов, пока профильная спецификация не определила service-owned settings; для `sms-service` runtime-параметры уже вынесены в `sms.sms_setting`.
|
||||
|
||||
**Запрещено в `.env` (→ только `app_settings`):**
|
||||
**Запрещено в `.env`:**
|
||||
|
||||
- credential-bearing DSN/URL;
|
||||
- пароли БД, Keycloak bootstrap password и OTP mock/HMAC secrets;
|
||||
- S3 access/secret keys;
|
||||
- Bitrix OAuth secrets и application/webhook tokens;
|
||||
- service tokens внутренних API;
|
||||
- SMS API key/callback credentials и OTLP auth header;
|
||||
- TLS private keys и любые иные credentials.
|
||||
|
||||
Они доставляются как Compose/systemd secret files по arch-06. Бизнес-параметры ниже хранятся только в `app_settings`:
|
||||
|
||||
- включение/отключение OTP, OTP-лимиты для UI/продукта;
|
||||
- телефон оператора, consent URLs/versions;
|
||||
@@ -52,7 +62,13 @@ Managed PostgreSQL **поднимается до** развёртывания п
|
||||
|
||||
### `text_resources` / `popular_questions`
|
||||
|
||||
Контент UI — отдельные таблицы (не `app_settings`). Ключи MVP — TBD (спецификация frontend).
|
||||
Контент UI — отдельные таблицы (не `app_settings`). Обязательная safety-мнемоника MVP:
|
||||
|
||||
| mnemonic | Назначение |
|
||||
|---|---|
|
||||
| `safety.chat.blocked` | Generic company-реплика при любом Message Safety deny; текст locale-aware, без раскрытия `rule_id` |
|
||||
|
||||
Миграция/seed обязаны создать активную запись минимум для `ru`. Изменение `text_value` не требует redeploy Safety и не меняет API/error code.
|
||||
|
||||
---
|
||||
|
||||
@@ -131,6 +147,7 @@ chat.attachments.storage=selectel_s3
|
||||
chat.attachments.upload_mode=presigned_put
|
||||
chat.attachments.safety_scan_required=true
|
||||
chat.attachments.presigned_upload_ttl_seconds=600
|
||||
messages.max_text_length=4000
|
||||
|
||||
rate_limit.message_send.per_user=30/minute
|
||||
rate_limit.message_send.per_dialog=20/minute
|
||||
@@ -174,21 +191,32 @@ worker.poll_interval_ms=500
|
||||
worker.lease_seconds=90
|
||||
```
|
||||
|
||||
В `.env` остаются только `SMS_DATABASE_URL`, URL внутренних/внешних сервисов, service tokens, Direct API key и callback credentials. Детальный контракт — `module-11-idgtl-sms.md`.
|
||||
В `.env` остаются только несекретные URL внутренних/внешних сервисов и технические параметры. `SMS_DATABASE_URL`, service tokens, Direct API key и callback credentials входят в runtime secret catalog. Детальный контракт — `module-11-idgtl-sms.md`.
|
||||
|
||||
`<approved>` — обязательный deployment placeholder, а не допустимое production-значение. Перед real mode должны существовать active approved template `auth_otp` с точными placeholders `code`/`ttl_min` и согласованный `senderName`. Отсутствие template/sender делает readiness false.
|
||||
|
||||
---
|
||||
|
||||
## Service-owned настройки `message-safety`
|
||||
|
||||
Runtime policy хранится в версионированной `message_safety.config_versions`, а не в `.env` и не в `han_app.app_settings`. Сюда входят task lease/deadline/attempts, internal rate/pending limits, retention/cache TTL, URL/DNS pipeline limits, ClamAV policy timeout/signature age и enabled file MIME/size policy. Полный schema/seed/activation contract — module-05 §10.1 и §15.
|
||||
|
||||
`han_app.app_settings:chat.attachments.*` остаётся бизнес-настройкой api-backend. Message Safety не получает cross-schema read к `han_app`; файл допускается только при пересечении business allow-list, active safety policy и immutable detector manifest. Active policy может сузить manifest, но не добавить parser и не увеличить hard limit.
|
||||
|
||||
В env Message Safety остаются только bootstrap/topology/capacity (`APP_ENV`, worker concurrency, DNS resolver, ClamAV/S3/OTLP endpoints); credentials доставляются secret files. Rules/detector versions вычисляются/проверяются по immutable artifacts. Emergency MOCK остаётся в отдельном root-owned mode file и намеренно не переносится в БД.
|
||||
|
||||
---
|
||||
|
||||
## Пример `.env.example`
|
||||
|
||||
Только инфраструктура. Бизнес-параметры — в seed `app_settings`.
|
||||
Только несекретная инфраструктура и выбор secret source. Бизнес-параметры — в seed `app_settings`, а перечисленные ниже runtime secrets — в конфигурации `han-secrets`, не в этом файле.
|
||||
|
||||
```text
|
||||
# =============================================================================
|
||||
# Общие
|
||||
# =============================================================================
|
||||
APP_ENV=production-like
|
||||
SECRETS_SOURCE=selectel
|
||||
API_PORT=8000
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
@@ -199,14 +227,10 @@ HAN_PG_HOST=<managed-pg-private-host>
|
||||
HAN_PG_PORT=5433
|
||||
HAN_PG_DATABASE=han_chat
|
||||
|
||||
DATABASE_URL=postgresql+asyncpg://han_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
BITRIX_DATABASE_URL=postgresql://bitrix_local_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
BITRIX_SYNC_APP_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
SMS_DATABASE_URL=postgresql://sms_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
|
||||
KEYCLOAK_DB_URL=jdbc:postgresql://<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?user=keycloak_user&password=change-me¤tSchema=keycloak
|
||||
KC_DB_URL_PROPERTIES=currentSchema=keycloak
|
||||
# Runtime secrets: DATABASE_URL, BITRIX_DATABASE_URL,
|
||||
# BITRIX_SYNC_APP_DATABASE_URL, BITRIX_SYNC_DATABASE_URL,
|
||||
# MESSAGE_SAFETY_DATABASE_URL, SMS_DATABASE_URL, KEYCLOAK_DB_URL.
|
||||
# Selectel PgBouncer 5433: pool_mode=session; search_path задаётся на уровне ролей.
|
||||
# Не добавлять options=-csearch_path: pooler отклоняет этот startup parameter.
|
||||
|
||||
@@ -245,51 +269,45 @@ KEYCLOAK_INTERNAL_URL=http://keycloak:8080
|
||||
KEYCLOAK_REALM=han-chat
|
||||
KEYCLOAK_AUDIENCE=han-chat-api
|
||||
KEYCLOAK_OTP_MOCK_ENABLED=true
|
||||
KEYCLOAK_OTP_MOCK_CODE=1234
|
||||
KEYCLOAK_YANDEX_CAPTCHA_ENABLED=false
|
||||
KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY=
|
||||
KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=
|
||||
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
|
||||
# Runtime secrets: KEYCLOAK_OTP_MOCK_CODE,
|
||||
# KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY and OTP HMAC/bootstrap credentials.
|
||||
|
||||
# =============================================================================
|
||||
# Redis (I4: раздельные DB index)
|
||||
# Redis ВМ1 (I4: раздельные DB index)
|
||||
# =============================================================================
|
||||
# /0 — api-backend: rate limits, idempotency
|
||||
# /1 — api-backend realtime/coordination (опционально; можно совместить с /0)
|
||||
# /2 — message-safety: verdict cache / workers
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
REDIS_REALTIME_URL=redis://redis:6379/1
|
||||
MESSAGE_SAFETY_REDIS_URL=redis://redis:6379/2
|
||||
|
||||
# =============================================================================
|
||||
# Service tokens (internal API) — все переменные только в backend/.env
|
||||
# Service tokens (internal API) — runtime secret catalog, не .env
|
||||
# =============================================================================
|
||||
MESSAGE_SAFETY_SERVICE_TOKEN=change-me
|
||||
BITRIX_LOCAL_APP_INTERNAL_TOKEN=change-me
|
||||
BITRIX_API_INBOX_TOKEN=change-me
|
||||
BITRIX_INTERNAL_API_TOKEN=change-me
|
||||
BITRIX_API_FORWARD_TOKEN=change-me
|
||||
BITRIX_SYNC_SERVICE_TOKEN=change-me
|
||||
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=change-me
|
||||
SMS_SERVICE_TOKEN=change-me
|
||||
KEYCLOAK_SMS_SERVICE_TOKEN=change-me
|
||||
NOTIFICATIONS_TOKEN_PRODUCER_TEST=change-me
|
||||
# MESSAGE_SAFETY_SERVICE_TOKEN, BITRIX_LOCAL_APP_INTERNAL_TOKEN,
|
||||
# BITRIX_API_INBOX_TOKEN, BITRIX_INTERNAL_API_TOKEN,
|
||||
# BITRIX_API_FORWARD_TOKEN, BITRIX_SYNC_SERVICE_TOKEN,
|
||||
# KEYCLOAK_SETTINGS_BRIDGE_TOKEN, SMS_SERVICE_TOKEN,
|
||||
# KEYCLOAK_SMS_SERVICE_TOKEN, NOTIFICATIONS_TOKEN_PRODUCER_TEST.
|
||||
|
||||
# =============================================================================
|
||||
# SMS provider (URL и секреты; runtime-параметры — sms.sms_setting)
|
||||
# SMS provider (секреты — runtime secret catalog; параметры — sms.sms_setting)
|
||||
# =============================================================================
|
||||
IDGTL_SMS_BASE_URL=https://direct.i-dgtl.ru
|
||||
IDGTL_SMS_API_KEY=change-me
|
||||
IDGTL_SMS_CALLBACK_PUBLIC_URL=https://tohin.ru/callbacks/idgtl/sms
|
||||
IDGTL_SMS_CALLBACK_USERNAME=change-me
|
||||
IDGTL_SMS_CALLBACK_PASSWORD=change-me
|
||||
# Runtime secrets: IDGTL_SMS_API_KEY, IDGTL_SMS_CALLBACK_USERNAME,
|
||||
# IDGTL_SMS_CALLBACK_PASSWORD.
|
||||
|
||||
# =============================================================================
|
||||
# api-backend (интеграции + resilience I2)
|
||||
# =============================================================================
|
||||
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
|
||||
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
|
||||
MESSAGE_SAFETY_URL=http://message-safety:8080
|
||||
MESSAGE_SAFETY_URL=https://processing.internal:8443
|
||||
MESSAGE_SAFETY_CA_FILE=/run/han-chat/secrets/processing-internal-ca.crt
|
||||
MESSAGE_SAFETY_API_PREFIX=/internal/safety/v2
|
||||
MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD=5
|
||||
MESSAGE_SAFETY_CIRCUIT_OPEN_SEC=30
|
||||
BITRIX_LOCAL_APP_CIRCUIT_FAILURE_THRESHOLD=5
|
||||
@@ -300,35 +318,58 @@ BITRIX_LOCAL_APP_HTTP_TIMEOUT_SEC=10
|
||||
# bitrix-sync
|
||||
# =============================================================================
|
||||
BITRIX_SYNC_ENABLED=true
|
||||
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
|
||||
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
|
||||
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
|
||||
BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC=30
|
||||
BITRIX_SYNC_CRM_MAX_CONCURRENCY=2
|
||||
BITRIX_SYNC_CONTACT_LIST_BATCH_SIZE=50
|
||||
BITRIX_SYNC_WEBHOOK_TOKEN=change-me
|
||||
BITRIX_SYNC_MODE=full
|
||||
BITRIX_SYNC_PORTAL_HOST=han0107.bitrix24.ru
|
||||
BITRIX_SYNC_PORTAL_MEMBER_ID=<approved-member-id>
|
||||
BITRIX_SYNC_PUBLIC_BASE_URL=https://processing.example.ru
|
||||
BITRIX_WEBHOOK_ALLOWED_CIDRS=<comma-separated-cidrs>
|
||||
BITRIX_SYNC_CONTACT_USER_ID_FIELD=UF_CRM_...
|
||||
BITRIX_SYNC_CONTACT_REGISTERED_FIELD=UF_CRM_1778692456
|
||||
BITRIX_SYNC_CONTACT_CITIZENSHIP_FIELD=UF_CRM_1768493029
|
||||
BITRIX_SYNC_HTTP_TIMEOUT_SEC=10
|
||||
BITRIX_SYNC_DB_POOL_SIZE=5
|
||||
# Hot worker/rate/retry/reconciliation/alert parameters:
|
||||
# versioned bitrix_sync.settings, не env.
|
||||
# Runtime secrets: BITRIX_SYNC_DATABASE_URL,
|
||||
# BITRIX_SYNC_CRM_REST_WEBHOOK_URL,
|
||||
# BITRIX_SYNC_CONTACT_RECEIVER_TOKEN,
|
||||
# BITRIX_SYNC_ALERT_RECEIVER_TOKEN,
|
||||
# BITRIX_SYNC_SERVICE_TOKEN.
|
||||
|
||||
# =============================================================================
|
||||
# bitrix-local-app
|
||||
# =============================================================================
|
||||
BITRIX_CLIENT_ID=change-me
|
||||
BITRIX_CLIENT_SECRET=change-me
|
||||
BITRIX_CONNECTOR_ID=han_mobile_app
|
||||
BITRIX_CONNECTOR_NAME=HAN Mobile App
|
||||
BITRIX_OPEN_LINE_ID=8
|
||||
BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix
|
||||
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
|
||||
BITRIX_APPLICATION_TOKEN=change-me
|
||||
# Runtime secrets: BITRIX_CLIENT_ID, BITRIX_CLIENT_SECRET,
|
||||
# BITRIX_APPLICATION_TOKEN and token encryption key.
|
||||
|
||||
# =============================================================================
|
||||
# message-safety (technical)
|
||||
# api-backend → Message Safety (ВМ1 caller)
|
||||
# =============================================================================
|
||||
# POST check timeout; poll interval/max — бюджет sync-wait внутри POST .../messages (G4)
|
||||
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
|
||||
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
|
||||
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
|
||||
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
|
||||
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
|
||||
QUARANTINE_ORPHAN_RETENTION_HOURS=48
|
||||
HAN_APP_SAFETY_CHECKPOINT_RETENTION_DAYS=7
|
||||
HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200
|
||||
|
||||
# =============================================================================
|
||||
# message-safety (ВМ2 bootstrap/topology/capacity)
|
||||
# =============================================================================
|
||||
# Service runtime policy находится в message_safety.config_versions.
|
||||
# Runtime secrets: MESSAGE_SAFETY_DATABASE_URL,
|
||||
# MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0,
|
||||
# MESSAGE_SAFETY_SERVICE_TOKEN, S3 quarantine read credentials.
|
||||
# Emergency mode находится только в root-owned
|
||||
# /etc/han-chat/message-safety-mode.env и меняется approved helper-ом.
|
||||
MESSAGE_SAFETY_WORKER_CONCURRENCY=5
|
||||
MESSAGE_SAFETY_DNS_RESOLVERS=<VPC-resolver-IP>
|
||||
MESSAGE_SAFETY_CLAMAV_HOST=clamd
|
||||
MESSAGE_SAFETY_CLAMAV_PORT=3310
|
||||
|
||||
# =============================================================================
|
||||
# Frontend (nginx)
|
||||
@@ -344,14 +385,14 @@ SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
|
||||
SELECTEL_S3_BUCKET_DOCUMENTS=han-chat-documents
|
||||
SELECTEL_S3_BUCKET_ATTACHMENTS=han-chat-attachments
|
||||
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
|
||||
SELECTEL_S3_ACCESS_KEY=change-me
|
||||
SELECTEL_S3_SECRET_KEY=change-me
|
||||
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=change-me
|
||||
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=change-me
|
||||
# Runtime secrets: SELECTEL_S3_ACCESS_KEY, SELECTEL_S3_SECRET_KEY,
|
||||
# SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY,
|
||||
# SELECTEL_S3_QUARANTINE_READ_SECRET_KEY.
|
||||
|
||||
# =============================================================================
|
||||
# Observability
|
||||
# =============================================================================
|
||||
# На каждой VM это local Docker DNS; ВМ2 не указывает collector ВМ1.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||
OTEL_SERVICE_NAME_API=api-backend
|
||||
OTEL_SERVICE_NAME_SMS_API=sms-service
|
||||
@@ -359,28 +400,28 @@ OTEL_SERVICE_NAME_SMS_WORKER=sms-worker
|
||||
OTEL_TRACES_SAMPLER=always_on
|
||||
SMS_METRICS_PORT=9464
|
||||
OTEL_REMOTE_ENDPOINT=192.168.0.5:4317
|
||||
OTEL_REMOTE_AUTH_HEADER=
|
||||
OTEL_REMOTE_TLS_INSECURE=true
|
||||
OTEL_QUEUE_SIZE=10000
|
||||
# Runtime secret when configured: OTEL_REMOTE_AUTH_HEADER.
|
||||
```
|
||||
|
||||
S3-клиенты используют только virtual-hosted addressing
|
||||
(`https://<bucket>.s3.storage.selcloud.ru/<object-key>`). Это часть контракта
|
||||
presigned URL и CORS Selectel; path-style адресация не поддерживается приложением.
|
||||
|
||||
Все переменные — **только** в `backend/.env`. Отдельного хранилища нет.
|
||||
Production использует отдельные allow-listed env manifests ВМ1 и ВМ2; не все переменные примера копируются на оба хоста. На ВМ2 `han-secrets` под отдельным IAM principal материализует отдельный root-owned файл каждому сервису в `/run/han-chat/secrets`; общий bundle ВМ1/ВМ2 запрещён.
|
||||
|
||||
Для production `change-me`, `<...>`, примерные sender/template/API key/callback credentials отклоняются `validate-env`. `IDGTL_SMS_API_KEY` — выданный Direct готовый `TOKEN_1` для Basic, без повторного Base64. Реальный статический egress IP хранится в deployment inventory, а не env; если он не обеспечен NAT/сетевой конфигурацией, `KEYCLOAK_OTP_MOCK_ENABLED=false` запрещён.
|
||||
Для production placeholders и примерные sender/template/credentials отклоняются `validate-env` и runtime manifest validation. `IDGTL_SMS_API_KEY` в secret catalog — выданный Direct готовый `TOKEN_1` для Basic, без повторного Base64. Реальный статический egress IP хранится в deployment inventory, а не env; если он не обеспечен NAT/сетевой конфигурацией, `KEYCLOAK_OTP_MOCK_ENABLED=false` запрещён.
|
||||
|
||||
**Webhook-токены** (публичные callback, не service API): `BITRIX_APPLICATION_TOKEN`, `BITRIX_SYNC_WEBHOOK_TOKEN`.
|
||||
**Webhook-токены:** `BITRIX_APPLICATION_TOKEN` относится только к `bitrix-local-app`. CRM sync не использует local app/event handler; штатные HTTP-webhook роботы передают отдельные `BITRIX_SYNC_CONTACT_RECEIVER_TOKEN` и `BITRIX_SYNC_ALERT_RECEIVER_TOKEN` в query, поскольку custom Bearer header недоступен. Токены остаются secret-manager values, query исключается из logs/traces, а nginx дополнительно применяет `BITRIX_WEBHOOK_ALLOWED_CIDRS`. `BITRIX_SYNC_CRM_REST_WEBHOOK_URL` — отдельный секрет исходящего CRM REST-доступа sync-service.
|
||||
|
||||
`NOTIFICATIONS_TOKEN_<SOURCE>` — индивидуальный секрет продюсера Internal Notifications API. Для seed/smoke используется `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; secret хранится только в deployment env/secret, а `notification_sources.token_hash` — только hash. Инструкция не имеет `notification.instruction.allowed_hosts`: она всегда открывается в новой вкладке, iframe-режима нет.
|
||||
`NOTIFICATIONS_TOKEN_<SOURCE>` — индивидуальный секрет продюсера Internal Notifications API. Для seed/smoke используется `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; secret хранится только в secret store/runtime secret file, а `notification_sources.token_hash` — только hash. Инструкция не имеет `notification.instruction.allowed_hosts`: она всегда открывается в новой вкладке, iframe-режима нет.
|
||||
|
||||
## Namespace переменных Bitrix
|
||||
|
||||
- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens.
|
||||
- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL`, circuit/timeout vars, Redis `/0`/`/1` + service tokens; **бизнес-настройки** — из `app_settings`. OTP counters **не** ведёт.
|
||||
- `bitrix-sync`: `BITRIX_SYNC_ENABLED`, `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`.
|
||||
- `bitrix-sync`: non-secret `BITRIX_SYNC_ENABLED/MODE/PORTAL_HOST/PORTAL_MEMBER_ID`, `BITRIX_WEBHOOK_ALLOWED_CIDRS`, custom field names и capacity bootstrap; runtime secrets — database/inbound CRM REST webhook URL, Contact/alert receiver tokens и service token; hot policy — в `bitrix_sync.settings`.
|
||||
- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`.
|
||||
|
||||
### `BITRIX_SYNC_ENABLED`
|
||||
@@ -390,7 +431,13 @@ presigned URL и CORS Selectel; path-style адресация не поддер
|
||||
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
|
||||
| `false` | синхронизация с Bitrix24 CRM не выполняется; сервис стартует в no-op/degraded режиме; чат Open Lines через `bitrix-local-app` **не** затрагивается |
|
||||
|
||||
В MVP выбран режим **no-op service**: контейнер `bitrix-sync` стартует, `/health/live` отвечает успешно, `/health/ready` возвращает degraded/not-ready с явной причиной `sync_disabled`, worker не обрабатывает `sync_queue`, webhook CRM возвращает безопасный `503` или `202 ignored` по контракту модуля. Это сохраняет единый compose-контур и не влияет на чат Open Lines.
|
||||
При `false` контейнер остаётся live, `/health/ready` возвращает `503 sync_disabled`, worker/reconciliation не claim-ят работу. Public webhook не должен безусловно подтверждать событие как обработанное: до cutover endpoint закрывается на edge либо возвращает retryable `503`. Это не влияет на чат Open Lines.
|
||||
|
||||
### `bitrix_sync.settings`
|
||||
|
||||
Versioned hot settings содержат batch size/wait, claim size, lease TTL, portal limiter refill/burst, max in-flight, retry base/max/horizon, Contact/alert reconciliation intervals, пороги всплеска Contact, восстановленных без webhook, и IDs/стадии/поля/SLA smart process. Новая версия активируется только после полной type/range/cross-field validation; невалидная версия не заменяет последнюю рабочую.
|
||||
|
||||
Secrets, DSN, portal host/member ID, inbound source IP CIDR allow-list, custom Contact field names и cutover watermark не являются hot settings. Полный каталог и defaults — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md), §12.
|
||||
|
||||
## Keycloak settings bridge для OTP
|
||||
|
||||
@@ -414,6 +461,7 @@ Challenge сохраняет snapshot TTL, длины кода и `settings_vers
|
||||
| `chat.attachments.allowed_extensions` | `jpg`, `jpeg`, `png`, `webp`, `heic`, `heif`, `pdf` |
|
||||
| `chat.attachments.allowed_mime_types` | `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` |
|
||||
| `chat.attachments.max_size_mb` | `5` |
|
||||
| `messages.max_text_length` | `4000` (public/business limit; Safety hard ceiling остаётся `10000`) |
|
||||
|
||||
Правило: файл принимается только если **и** расширение, **и** MIME в allow-list. Детальная проверка — модуль `message-safety`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# arch-05. Правила разработки модулей отдельными агентами
|
||||
|
||||
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md). Настоящий документ описывает процесс разработки и не переопределяет архитектуру. Иерархия приоритета — в [`README.md`](README.md), раздел «Разрешение конфликтов».
|
||||
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md). Безопасность VM и production-деплоя — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). Настоящий документ описывает процесс разработки и не переопределяет архитектуру. Иерархия приоритета — в [`README.md`](README.md), раздел «Разрешение конфликтов».
|
||||
|
||||
## Цель
|
||||
|
||||
@@ -11,10 +11,19 @@
|
||||
- Перед разработкой агент читает [`README.md`](README.md), архитектурные документы и профильный документ назначенного модуля.
|
||||
- Любое изменение публичного API сопровождается обновлением OpenAPI.
|
||||
- Любое изменение структуры данных сопровождается миграцией.
|
||||
- Все **бизнес-параметры** — в таблице `app_settings`; **infra и секреты** — в `.env`.
|
||||
- Все **бизнес-параметры** — в таблице `app_settings`; несекретная **infra** — в `.env`; production-секреты — только через механизм arch-06.
|
||||
- Нельзя hardcode-ить телефоны, лимиты, тексты, mime types, feature flags и параметры Битрикс24.
|
||||
- Модули, принимающие пользовательский ввод, должны учитывать rate limits и security/safety проверки.
|
||||
|
||||
## Размещение и production-деплой
|
||||
|
||||
- Изменение deployment, VM topology, network exposure, volumes, Linux capabilities, OS/sudo-прав или способа доставки секретов требует impact analysis по arch-06.
|
||||
- Агент не добавляет `deploy` в группу `docker` и не расширяет sudo wildcard-командами. Новое право оформляется как конкретная операция над конкретным systemd-unit с review и rollback.
|
||||
- Production compose, systemd-units, deploy scripts и secret mappings остаются root-owned и недоступны `deploy` на запись.
|
||||
- Для private/no-egress VM допустим временный bootstrap с SSH из trusted ops CIDR и ограниченным egress для пакетов/образов.
|
||||
- Раскатка private/no-egress VM считается незавершённой, пока не выполнен lockdown: public ingress/SSH и общий egress закрыты, private access проверен, а недоступность снаружи зафиксирована.
|
||||
- Повторное открытие ingress/egress после lockdown — документированная break-glass операция с обязательным возвратом в lockdown, а не штатный способ деплоя.
|
||||
|
||||
## Правила базы данных
|
||||
|
||||
- Перечень таблиц, полей, индексов и миграций **определяет модуль-владелец** (`database`, `api-backend`, `bitrix-sync`, `message-safety`, `bitrix-local-app`), а не arch-*.
|
||||
@@ -39,6 +48,7 @@
|
||||
Правила:
|
||||
|
||||
- endpoint naming должен следовать `arch-02-api-contracts.md`;
|
||||
- cross-VM contract test обязан запускать caller и callee как разные network zones: private DNS, verified internal CA, service token, timeout/circuit и запрет plaintext; Docker hostname вынесенного сервиса не считается валидным remote test;
|
||||
- response schema не должна раскрывать внутренние поля;
|
||||
- ошибки возвращаются в едином формате;
|
||||
- для пользовательских данных всегда используется текущий user context из JWT;
|
||||
@@ -86,8 +96,13 @@ Raw OTP запрещено хранить в открытом виде: это
|
||||
- обновлены каталог ошибок в `arch-02` и contract tests, если менялась публичная или internal HTTP-семантика;
|
||||
- созданы миграции, если менялась БД;
|
||||
- обновлены seed `app_settings` и `.env.example`, если добавлялись настройки, service tokens, лимиты или feature flags;
|
||||
- secret value не добавлен в `.env.example`; новый секрет включён только в runtime secret catalog и выдан минимальному набору сервисов;
|
||||
- добавлены тесты;
|
||||
- сервис запускается в Docker Compose;
|
||||
- сервис запускается в root Docker Compose своей VM; CI отдельно валидирует оба projects и отсутствие cross-host `depends_on`/Docker DNS;
|
||||
- remote Message Safety contract tests покрывают v2 `202 + Location + Retry-After`, sticky final result, terminal failed `503`, `409` invariant mapping и legacy v1 migration adapter;
|
||||
- container проверен по arch-06: non-root, `no-new-privileges`, capabilities, read-only filesystem/writable paths, volumes, networks и resource limits;
|
||||
- изменение прав `deploy`, systemd, network exposure, capabilities, volumes или secret delivery отражено в arch-06 и deployment runbook;
|
||||
- для private/no-egress VM выполнен и зафиксирован bootstrap→lockdown checklist;
|
||||
- worker, указанный в Compose/runbook, имеет реально зарегистрированный entrypoint в image; deployment не может заранее выдумывать имя команды;
|
||||
- все изменяемые параметры вынесены из кода;
|
||||
- логи содержат `request_id`, `trace_id` и **`ux_session_id`** (если передан в запросе);
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
# arch-06. Стандарт безопасности размещения сервисов
|
||||
|
||||
> Общие границы системы и прикладная безопасность — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md). Docker Compose, nginx и сети контейнеров — в [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Настройки и секреты — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Процесс разработки и Definition of Done — в [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md).
|
||||
|
||||
## Назначение
|
||||
|
||||
Документ задаёт обязательный минимальный стандарт размещения сервисов HAN на виртуальных машинах, подготовки VM и production-деплоя.
|
||||
|
||||
Главная цель — ограничить последствия компрометации отдельного сервиса: захват процесса или контейнера не должен автоматически давать доступ к host OS, Docker daemon, соседним сервисам, чужим секретам или всей private network.
|
||||
|
||||
Стандарт применяется к:
|
||||
|
||||
- VM с публичной точкой входа;
|
||||
- внутренним VM в private network;
|
||||
- VM без постоянного доступа в интернет, включая SigNoz;
|
||||
- пользователям `deploy`, `admin` и `tunnel`;
|
||||
- systemd-юнитам, Docker Compose и deployment-артефактам.
|
||||
|
||||
Если конкретный сервис не может выполнить требование, отклонение должно быть явно описано в его спецификации: причина, риск, компенсирующая мера, владелец и срок пересмотра. Молчаливое ослабление требований запрещено.
|
||||
|
||||
## Модель угроз и границы доверия
|
||||
|
||||
Базовое допущение: атакующий может добиться выполнения кода внутри одного прикладного контейнера.
|
||||
|
||||
После этого он не должен получить:
|
||||
|
||||
- доступ к Docker socket или Docker API;
|
||||
- root на host OS;
|
||||
- возможность менять compose-файлы, systemd-юниты, deployment-скрипты или `sudoers`;
|
||||
- секреты сервисов, которые не нужны скомпрометированному процессу;
|
||||
- произвольный доступ к PostgreSQL, S3 и другим VM;
|
||||
- возможность публиковать новый host port или подключать host directories;
|
||||
- постоянный канал управления через неограниченный исходящий трафик.
|
||||
|
||||
Изоляция строится несколькими независимыми слоями: IAM и секреты, Unix-права, systemd/sudo, настройки контейнера, Docker networks, host firewall и cloud security groups. Один слой не считается заменой остальных.
|
||||
|
||||
## Классы VM
|
||||
|
||||
### VM с постоянным egress
|
||||
|
||||
VM имеет только утверждённые исходящие направления, необходимые сервисам: Selectel Secrets Manager, S3, внешние API, package/image registry и DNS/NTP по принятой схеме.
|
||||
|
||||
Постоянный egress не означает unrestricted internet access. Направления и назначение фиксируются в deployment inventory; лишние правила удаляются.
|
||||
|
||||
### Private/no-egress VM
|
||||
|
||||
В steady state VM:
|
||||
|
||||
- не принимает соединения из интернета;
|
||||
- не имеет общего выхода в интернет;
|
||||
- принимает только явно разрешённый трафик из private network;
|
||||
- администрируется через утверждённую private точку входа: bastion/основную VM или VPN.
|
||||
|
||||
SigNoz относится к этому классу, если его UI, OTLP и SSH доступны только из private network.
|
||||
|
||||
### Каноническая классификация ВМ2 Processing
|
||||
|
||||
ВМ2 с `message-safety` и `bitrix-sync` — **самостоятельная service VM с минимальным public ingress и постоянным ограниченным egress**:
|
||||
|
||||
- собственный public DNS/IP или dedicated LB направляет `80/443` только на nginx ВМ2;
|
||||
- public `80` обслуживает только ACME challenge/HTTPS redirect;
|
||||
- public `443` разрешает только exact `/bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert`; остальные paths закрыты;
|
||||
- private ingress `8443/tcp` разрешён только от security group ВМ1 и утверждённого ops path для Message Safety/internal API;
|
||||
- ни один public запрос ВМ2 не проходит через nginx ВМ1;
|
||||
- `freshclam` имеет egress только к утверждённым источникам сигнатур;
|
||||
- `bitrix-sync` имеет HTTPS egress только к утверждённому порталу Bitrix24;
|
||||
- Safety worker имеет доступ только к managed PostgreSQL, S3-quarantine и доверенному DNS resolver;
|
||||
- локальный OTEL Collector имеет private egress к SigNoz;
|
||||
- registry, package repositories и OS updates открываются только в bootstrap/controlled maintenance window.
|
||||
|
||||
ВМ2 использует отдельный cloud IAM principal. `han-secrets` получает только секреты сервисов ВМ2 и материализует раздельные root-owned файлы на tmpfs; общий secret bundle с ВМ1 запрещён. На ВМ2 один root Compose project и отдельный root-owned systemd deployment unit.
|
||||
|
||||
Для схемы `message_safety` разделяются DB roles: API/worker runtime читает active/исторические `config_versions`, но не создаёт и не активирует их; migration/config-admin role используется только controlled job и имеет право version activation. Config не содержит secrets, endpoint topology или MOCK flags.
|
||||
|
||||
Public и private ingress ВМ2 завершаются разными server blocks одного nginx без общего fallback. Public block использует сертификат доверенного CA и до proxy ограничивает Contact/alert webhook version-controlled source IP CIDR allow-list. Штатный робот передаёт отдельный receiver token в query и `application/x-www-form-urlencoded` body; query/body исключаются из logs/traces, а upstream проверяет token и document/entity/domain/member fields. Server-to-server ingress `8443` использует внутренний CA и service token вторым слоем. mTLS не обязателен для MVP.
|
||||
|
||||
## Lifecycle private/no-egress VM
|
||||
|
||||
Этот lifecycle применяется к SigNoz и иным полностью private VM. Для ВМ2 обязательный public webhook ingress `80/443` после bootstrap не удаляется; вместо этого проверяются exact route и source IP CIDR allow-list, а SSH и все прочие public ports закрываются. Новый IP не добавляется автоматически: всплеск восстановлений Contact инкрементальной reconciliation инициирует проверку rejected-IP telemetry и controlled review allow-list.
|
||||
|
||||
Для первичной раскатки применяется двухфазный процесс.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Bootstrap[Bootstrap_phase]
|
||||
Verify[Verify_services_and_private_links]
|
||||
Lockdown[Steady_state_lockdown]
|
||||
Bootstrap -->|"temporary public SSH and package egress"| Verify
|
||||
Verify -->|"remove public ingress and egress"| Lockdown
|
||||
```
|
||||
|
||||
### Фаза bootstrap
|
||||
|
||||
На ограниченное время разрешаются:
|
||||
|
||||
- SSH из утверждённого trusted ops CIDR, а не из `0.0.0.0/0` - управляется через группу безопасности облачного провайдера;
|
||||
- egress, необходимый для обновлений ОС, установки пакетов и получения pinned images/artifacts;
|
||||
- доступ `deploy` и `admin` в пределах правил этого документа.
|
||||
|
||||
До перехода в lockdown необходимо:
|
||||
|
||||
1. установить обновления и минимальный набор пакетов;
|
||||
2. установить и проверить host firewall и fail2ban;
|
||||
3. развернуть сервисы и секреты;
|
||||
4. проверить health/readiness;
|
||||
5. проверить требуемые private-соединения в обоих направлениях;
|
||||
6. подтвердить альтернативный private путь администрирования;
|
||||
7. сохранить rollback-инструкцию и inventory разрешённых соединений.
|
||||
|
||||
### Фаза lockdown
|
||||
|
||||
После проверки:
|
||||
|
||||
- public IP удаляется, если он больше не нужен;
|
||||
- публичный SSH и любой иной internet ingress удаляются из cloud security group;
|
||||
- host firewall принимает административный и прикладной трафик только из утверждённых private CIDR/SG;
|
||||
- общий internet egress закрывается на cloud и host-уровне;
|
||||
- временные bootstrap credentials, правила, installer-файлы и package caches удаляются, если они больше не нужны;
|
||||
- с внешней сети проверяется недоступность SSH и сервисных портов;
|
||||
- с VM проверяется запрет неразрешённого egress;
|
||||
- из private network повторно проверяются SSH и обязательные service flows.
|
||||
|
||||
Раскатка private/no-egress VM не завершена, пока lockdown и обе группы проверок не зафиксированы в deployment checklist.
|
||||
|
||||
### Повторное открытие
|
||||
|
||||
Временное открытие ingress/egress после lockdown — break-glass операция:
|
||||
|
||||
1. фиксируются причина, исполнитель, окно работ и необходимые destination/ports;
|
||||
2. правило ограничивается trusted CIDR и минимальным сроком;
|
||||
3. после работ правила удаляются;
|
||||
4. повторяются проверки lockdown;
|
||||
5. факт закрытия фиксируется в runbook/журнале изменений.
|
||||
|
||||
Постоянно оставлять bootstrap-доступ «для будущих обновлений» запрещено.
|
||||
|
||||
## Пользователи host OS
|
||||
|
||||
### `deploy`
|
||||
|
||||
Используется для штатного деплоя. Пользователь:
|
||||
|
||||
- не входит в группы `docker`, `root` и другие root-equivalent группы;
|
||||
- не имеет общего `sudo`, shell root и `sudoedit`;
|
||||
- не меняет compose-файлы, systemd-юниты, deployment-скрипты и конфигурацию секретов;
|
||||
- может записывать только в выделенный incoming/staging-каталог;
|
||||
- может запускать только заранее утверждённые операции над конкретными systemd-юнитами;
|
||||
- на ВМ2 может запускать пять exact-argument вариантов root-owned Message Safety mode helper;
|
||||
- читает только логи своего стека, без доступа к секретам других сервисов.
|
||||
|
||||
Членство в группе `docker` считается эквивалентом root и запрещено.
|
||||
|
||||
### `admin`
|
||||
|
||||
Break-glass пользователь для восстановления:
|
||||
|
||||
- не используется для штатного деплоя;
|
||||
- имеет персональные SSH-ключи, а не общий ключ команды;
|
||||
- доступен только из trusted ops network/VPN, а для private VM — только через private path после lockdown;
|
||||
- расширенные sudo-права выдаются осознанно и аудируются;
|
||||
- ключи хранятся отдельно от deploy credentials и регулярно пересматриваются.
|
||||
|
||||
Доступ через cloud console/recovery mode также считается break-glass и должен быть ограничен ролями облачного проекта.
|
||||
|
||||
### `tunnel`
|
||||
|
||||
Отдельный пользователь основной/bastion VM для доступа к PostgreSQL и другим private endpoints:
|
||||
|
||||
- не имеет sudo;
|
||||
- не входит в deployment-группы;
|
||||
- не получает доступ к секретам приложения;
|
||||
- разрешает только local TCP forwarding;
|
||||
- имеет allow-list конкретных `host:port` через `PermitOpen`;
|
||||
- использует login shell `/usr/sbin/nologin` (после отдельной проверки, что forwarding-only соединение работает);
|
||||
- не разрешает agent forwarding, X11 forwarding и TTY;
|
||||
- ключ ограничивается теми же возможностями в `authorized_keys`.
|
||||
|
||||
Произвольный SOCKS proxy и forwarding на неутверждённые адреса запрещены. Настройка должна быть проверена отдельной SSH-сессией до отключения старого пути.
|
||||
|
||||
### Root
|
||||
|
||||
- `PermitRootLogin no`;
|
||||
- пароль root заблокирован (`passwd -l root`) как дополнительная мера;
|
||||
- штатные операции выполняются через именных пользователей;
|
||||
- прямой root допускается только механизмом recovery провайдера при инциденте.
|
||||
|
||||
## SSH baseline
|
||||
|
||||
Минимальные настройки production VM:
|
||||
|
||||
```text
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
MaxAuthTries 3
|
||||
AllowAgentForwarding no
|
||||
X11Forwarding no
|
||||
AllowUsers deploy admin tunnel
|
||||
```
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- SSH для Private/no-egress VM разрешается cloud SG и host firewall только из trusted ops CIDR/VPN/private network;
|
||||
- ключи пользователей индивидуальны; общий приватный ключ запрещён;
|
||||
- устаревшие алгоритмы и пустые пароли запрещены;
|
||||
- fail2ban включается на VM, где SSH хотя бы временно доступен из интернета;
|
||||
- `AllowTcpForwarding no` задаётся по умолчанию, а исключение `local` — только в `Match User tunnel`;
|
||||
- после изменения выполняется проверка конфигурации sshd и вход во второй независимой сессии;
|
||||
- текущую рабочую сессию не закрывают до успешной проверки нового доступа.
|
||||
|
||||
`AllowUsers` должен содержать только реально созданные учётные записи. Неиспользуемая роль не создаётся «на будущее».
|
||||
|
||||
## Права `deploy` и production-деплой
|
||||
|
||||
### Управление только через systemd
|
||||
|
||||
`deploy` не запускает `docker`, `docker compose` или произвольные root-скрипты через sudo. Docker Compose запускается root-owned systemd-юнитом или root-owned deployment helper с фиксированным интерфейсом.
|
||||
|
||||
Sudoers хранится только в `/etc/sudoers.d/deploy` и проверяется через `visudo`. `/etc/sudoers` напрямую не редактируется.
|
||||
|
||||
Разрешения перечисляют полные команды и конкретные unit names без wildcard. Принципиальный пример:
|
||||
|
||||
```sudoers
|
||||
Cmnd_Alias HAN_STATUS = /usr/bin/systemctl --no-pager status han-stack.service
|
||||
Cmnd_Alias HAN_DEPLOY = /usr/bin/systemctl start han-deploy.service, \
|
||||
/usr/bin/systemctl restart han-stack.service
|
||||
Cmnd_Alias HAN_LOGS = /usr/bin/journalctl --no-pager -u han-stack.service
|
||||
Cmnd_Alias HAN_SAFETY_MODE = /usr/local/sbin/han-message-safety-mode standard, \
|
||||
/usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true, \
|
||||
/usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false, \
|
||||
/usr/local/sbin/han-message-safety-mode mock --text-free false --file-free true, \
|
||||
/usr/local/sbin/han-message-safety-mode mock --text-free false --file-free false
|
||||
deploy ALL=(root) NOPASSWD: HAN_STATUS, HAN_DEPLOY, HAN_LOGS, HAN_SAFETY_MODE
|
||||
```
|
||||
|
||||
Фактические пути сверяются через `command -v`; разрешается только необходимый набор. Нельзя разрешать:
|
||||
|
||||
- `systemctl *`, `journalctl *`, wildcard в unit name;
|
||||
- `systemctl status`/`journalctl` с интерактивным pager (он может дать shell escape под root);
|
||||
- shell, editor, package manager, `cp`, `mv`, `chmod`, `chown`;
|
||||
- произвольный путь к compose-файлу или environment-файлу;
|
||||
- команды с параметрами, позволяющими подменить unit, working directory, image или mount.
|
||||
|
||||
`han-message-safety-mode` — исключение с конечным exact-argument allow-list, а не произвольный root-script. Он принадлежит `root:root`, недоступен `deploy` на запись, не принимает paths/commands/env expansion, атомарно меняет только `root:han-message-safety 0640` `/etc/han-chat/message-safety-mode.env`; dedicated host group имеет GID `10001`, совпадающий с primary GID non-root контейнера. Helper валидирует конфигурацию и выполняет только фиксированную Message Safety API recreate/restart operation внутри root Compose project. MOCK не имеет автоматического срока действия; выключение — отдельная явная команда `standard`. Все вызовы и old/new mode аудируются.
|
||||
|
||||
### Ownership deployment-файлов
|
||||
|
||||
Root-owned и недоступны `deploy` на запись:
|
||||
|
||||
- `/etc/systemd/system/han-*.service`;
|
||||
- production compose-файлы;
|
||||
- deploy/helper scripts;
|
||||
- `/etc/han-chat/message-safety-mode.env`;
|
||||
- `/etc/sudoers.d/deploy`;
|
||||
- secret mappings и credentials;
|
||||
- active release manifest.
|
||||
|
||||
`deploy` может загружать артефакты только в отдельный каталог, например `/var/lib/han-deploy/incoming`, без права менять его parent. Активация выполняется фиксированным root-owned процессом после проверок:
|
||||
|
||||
- артефакт относится к ожидаемому проекту и версии;
|
||||
- digest/signature соответствует approved release;
|
||||
- отсутствуют symlink/path traversal;
|
||||
- compose config прошёл валидацию;
|
||||
- image reference pinned по version/digest;
|
||||
- миграции и rollout соответствуют release manifest.
|
||||
|
||||
Если такого валидатора пока нет, compose/unit changes выполняет `admin`, а `deploy` ограничивается запуском уже подготовленного релиза. Выдавать `deploy` запись в production compose — не допустимая замена автоматизации.
|
||||
|
||||
Shell-артефакты (`*.sh`, entrypoint, hooks и helpers) обязаны поставляться с
|
||||
LF line endings. Репозиторий фиксирует это через `.gitattributes`, а release
|
||||
preflight проверяет отсутствие `CRLF` до активации. Ошибка вида
|
||||
`cannot execute: required file not found` при существующем executable-файле
|
||||
считается признаком некорректного shebang/line endings, а не основанием менять
|
||||
права или запускать файл через обходной интерпретатор.
|
||||
|
||||
## Изменение прав `deploy`
|
||||
|
||||
Новая доработка не получает дополнительные права автоматически. В change request указываются:
|
||||
|
||||
1. требуемая операция и конкретный systemd unit;
|
||||
2. почему существующего интерфейса недостаточно;
|
||||
3. полный executable path и фиксированные аргументы;
|
||||
4. какие root-owned файлы читает или меняет операция;
|
||||
5. возможность command/path/argument injection;
|
||||
6. тест негативных сценариев;
|
||||
7. способ отзыва права и rollback.
|
||||
|
||||
Изменение:
|
||||
|
||||
- проходит review владельца инфраструктуры/безопасности;
|
||||
- вносится отдельным файлом в `/etc/sudoers.d`;
|
||||
- проверяется `visudo`;
|
||||
- сначала проверяется в production-like среде;
|
||||
- отражается в этом документе и deployment runbook;
|
||||
- после rollout подтверждается через `sudo -l`, что лишних прав нет.
|
||||
|
||||
Wildcard, временный `NOPASSWD: ALL` и включение в `docker` group запрещены даже как «временное» решение.
|
||||
|
||||
## Секреты
|
||||
|
||||
### Общие требования
|
||||
|
||||
- секреты не коммитятся и не хранятся в обычном `.env`;
|
||||
- `.env` содержит только несекретную конфигурацию и ссылки/имена secret files;
|
||||
- контейнер получает только необходимые ему секреты;
|
||||
- общий файл со всеми секретами стека не монтируется во все контейнеры;
|
||||
- секреты не передаются в command line, build args, image layers и логи;
|
||||
- runtime secret files доступны только root и целевому process UID/GID;
|
||||
- ротация не требует выдачи сервису доступа к чужим секретам;
|
||||
- приложение не выступает сетевым прокси секретов для других VM.
|
||||
|
||||
Значения `uid`, `gid` и `mode` в Compose file secrets нельзя считать
|
||||
security boundary: Docker Compose при bind-backed secret может их игнорировать.
|
||||
Фактические owner/mode задаются host-side materializer'ом и проверяются через
|
||||
`stat` и негативный тест от постороннего UID. Предупреждение Compose об
|
||||
игнорировании этих атрибутов не подавляется и не трактуется как подтверждение
|
||||
прав.
|
||||
|
||||
Структурированные секреты валидируются до старта потребителя. Для PEM это
|
||||
означает проверку парсинга certificate/private key, отсутствие повторного
|
||||
base64 или литеральных `\n`, соответствие public key и запрет зашифрованного
|
||||
private key, если сервис не поддерживает non-interactive passphrase.
|
||||
|
||||
### VM с egress: `han-secrets`
|
||||
|
||||
На VM с утверждённым доступом к Selectel:
|
||||
|
||||
- один host-side `han-secrets` запускается через systemd до старта стека;
|
||||
- используется отдельный IAM principal на VM/контур;
|
||||
- IAM разрешает чтение только секретов сервисов этой VM;
|
||||
- контейнеры не получают cloud IAM credentials и сами не обращаются в Secrets Manager;
|
||||
- materialized secrets размещаются в `/run/han-chat/secrets` на tmpfs;
|
||||
- ошибка получения обязательного секрета блокирует rollout (fail closed);
|
||||
- автоматический fallback с Selectel на локальный production-файл запрещён.
|
||||
|
||||
Использование единой реализации `han-secrets` на нескольких VM допустимо; общая IAM-учётная запись и общий набор секретов — нет.
|
||||
|
||||
### Private/no-egress VM
|
||||
|
||||
Cloud sync не требуется. `admin` во время bootstrap:
|
||||
|
||||
- получает минимальный набор секретов по защищённому каналу;
|
||||
- размещает их в root-owned каталоге вне репозитория;
|
||||
- задаёт каталогам `0700`, файлам `0400` или более узкие ACL для целевого UID;
|
||||
- по возможности передаёт их процессу как systemd credentials или read-only secret files;
|
||||
- удаляет временную копию и историю команд;
|
||||
- фиксирует fingerprint/version секрета без его значения.
|
||||
|
||||
Допускается `han-secrets` в локальном `file`-режиме как единый loader, но он не должен создавать egress или зависимость от основной app-VM. Для ротации используется повторная контролируемая provisioning-процедура.
|
||||
|
||||
## DB roles и migration boundary
|
||||
|
||||
Runtime, migration и config-admin роли разделяются для каждого сервиса.
|
||||
Runtime-role не получает DDL, ownership схемы или право менять immutable
|
||||
configuration. Migration-role монтируется только в controlled job и не
|
||||
передаётся runtime-контейнерам.
|
||||
|
||||
Cross-schema migration не получает постоянный broad access. Если ей нужно
|
||||
однократно перенести legacy data:
|
||||
|
||||
1. владелец исходной схемы или DB administrator выдаёт именованной
|
||||
migration-role минимальные временные `USAGE` на schema и `SELECT` на
|
||||
конкретную таблицу;
|
||||
2. migration копирует данные и fail-closed проверяет полноту переноса;
|
||||
3. владелец/администратор отзывает временные права после успешного commit.
|
||||
|
||||
Migration-role не выполняет `REVOKE`, `ALTER` или `DROP` на объекте чужого
|
||||
owner. Такие contract-операции принадлежат owner migration исходного сервиса
|
||||
либо отдельной административной процедуре. Проверку существования объекта
|
||||
нельзя реализовывать как «нет доступа — значит объекта нет»: permission error
|
||||
должен блокировать rollout, иначе legacy data может быть молча пропущена.
|
||||
|
||||
Alembic graph обязан сохранять известные ранее выданные revision IDs, включая
|
||||
no-op baseline revisions. Удаление revision из нового image при наличии её в
|
||||
`alembic_version` запрещено; совместимость обеспечивается bridge/no-op
|
||||
revision, а не ручным `stamp` или правкой production DB.
|
||||
|
||||
## Права на каталоги
|
||||
|
||||
Базовая модель:
|
||||
|
||||
| Путь | Владелец / режим | Назначение |
|
||||
|---|---|---|
|
||||
| `/opt/han-chat/releases/<version>` | `root:root`, `0755`/файлы `0644` | immutable release |
|
||||
| `/opt/han-chat/current` | `root:root` | active release link; меняет только deployment helper/admin |
|
||||
| `/var/lib/han-deploy/incoming` | `deploy:deploy`, `0750` | загрузка неактивированных артефактов |
|
||||
| `/etc/han` | `root:root`, `0750` или строже | конфигурация и secret mappings |
|
||||
| `/run/han-chat/secrets` | `root:root`, `0700` | runtime secrets на tmpfs |
|
||||
| `/var/lib/han-chat/public-tls` | `root:han-nginx-tls`, `0750`; key/cert `0640` | минимальный TLS staging для non-root edge |
|
||||
| `/var/lib/han-chat/acme` | `root:root`, `0755` | ACME webroot без private key |
|
||||
| `/var/lib/han-chat/<service>` | UID сервиса, минимальные права | service state |
|
||||
| `/var/log/han-chat` | root/service group, без world-read | host-side логи при необходимости |
|
||||
|
||||
Требования:
|
||||
|
||||
- world-writable каталоги в deployment path запрещены;
|
||||
- setuid/setgid binaries не добавляются без обоснования;
|
||||
- сервис не получает write к каталогу с executable/config, если ему нужен только state;
|
||||
- bind mounts задаются read-only, кроме явно выделенных state/upload paths;
|
||||
- backup-файлы и дампы получают не менее строгие права, чем исходные данные;
|
||||
- symlinks из writable каталога не используются привилегированным helper без безопасной проверки.
|
||||
|
||||
## Hardening контейнеров
|
||||
|
||||
Для каждого production-контейнера обязательна оценка и, где применимо, конфигурация:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
service:
|
||||
user: "10001:10001"
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,nodev
|
||||
```
|
||||
|
||||
Базовые правила:
|
||||
|
||||
- процесс запускается непривилегированным UID/GID;
|
||||
- root в контейнере допускается только с документированным обоснованием;
|
||||
- `privileged: true` запрещён;
|
||||
- `network_mode: host`, `pid: host`, `ipc: host` и `userns_mode: host` запрещены;
|
||||
- Docker socket/API не монтируется;
|
||||
- Linux capabilities удаляются все, затем точечно возвращаются необходимые;
|
||||
- root filesystem read-only; writable paths — отдельные volume/tmpfs;
|
||||
- mount host paths минимален и read-only;
|
||||
- default seccomp сохраняется; AppArmor/аналог провайдера включается, если доступен;
|
||||
- задаются CPU/memory/PID limits, restart policy и healthcheck;
|
||||
- сервис подключается только к необходимым Docker networks;
|
||||
- внешний `ports:` разрешён только утверждённой edge-точке;
|
||||
- image использует pinned version/digest, проходит vulnerability scan и не содержит package managers/compilers без необходимости;
|
||||
- секреты не копируются в image и не доступны healthcheck-команде.
|
||||
|
||||
Если контейнер не может работать с `read_only`, в спецификации перечисляются конкретные writable paths. Полное отключение `read_only` без анализа запрещено.
|
||||
|
||||
### Проверка совместимости image с hardening
|
||||
|
||||
Non-root UID сам по себе недостаточен. Для каждого pinned digest до rollout
|
||||
составляется inventory всех путей, куда пишет entrypoint и процесс:
|
||||
runtime/socket, cache/temp, generated config, logs и persistent state. Каждый
|
||||
путь получает отдельный volume/tmpfs с минимальным размером и явными
|
||||
`uid/gid/mode`; writable root filesystem, запуск root или возврат capabilities
|
||||
не используются как универсальный workaround.
|
||||
|
||||
Проверяется не только основной binary, но и image entrypoint. Если vendor image
|
||||
имеет отдельный unprivileged entrypoint, при принудительном `user` используется
|
||||
именно он. Root entrypoint, который делает `mkdir/chown`, несовместим с
|
||||
non-root + `read_only`, даже если сам daemon способен работать без root.
|
||||
|
||||
Изменение image digest повторяет эти проверки: tag/version, entrypoint,
|
||||
writable-path inventory, healthcheck semantics и фактический UID/GID считаются
|
||||
частью security contract образа.
|
||||
|
||||
### TLS для non-root edge
|
||||
|
||||
Root-only дерево ACME/Certbot не монтируется целиком в non-root nginx и не
|
||||
делается world-readable. Host-side root hook атомарно копирует только
|
||||
`fullchain.pem` и `privkey.pem` в выделенный staging-каталог с группой
|
||||
`han-nginx-tls` (канонический GID `11001`); nginx получает этот каталог
|
||||
read-only. Renewal hook сначала обновляет staged files, затем выполняет полный
|
||||
config test и только после успеха отправляет reload. Права и соответствие
|
||||
certificate/key проверяются preflight.
|
||||
|
||||
### Daemon и updater как разные security-профили
|
||||
|
||||
Если один vendor image используется для daemon и updater, им задаются разные
|
||||
сети, mounts и health semantics. Проверенный паттерн ClamAV:
|
||||
|
||||
- `clamd` не имеет signature-CDN egress, читает signatures read-only и имеет
|
||||
healthcheck реального daemon socket;
|
||||
- `freshclam` один получает ограниченный egress и write к signatures;
|
||||
- оба используют vendor `init-unprivileged` и только выделенные writable
|
||||
`/run/clamav`, `/var/log/clamav` и `/tmp`;
|
||||
- updater запускается как постоянный foreground daemon, чтобы restart policy
|
||||
не превращала успешный one-shot exit в download loop/rate limit;
|
||||
- унаследованный healthcheck, проверяющий отсутствующий в updater-контейнере
|
||||
daemon, отключается; updater контролируется по `Up`, restart count, логам и
|
||||
возрасту сигнатур.
|
||||
|
||||
Ошибки `read-only file system` устраняются точечным writable mount. Запрещено
|
||||
лечить их глобальным `read_only: false`, root, `privileged` или broad
|
||||
capability.
|
||||
|
||||
## Сетевые ограничения
|
||||
|
||||
### Cloud и host
|
||||
|
||||
Используются одновременно:
|
||||
|
||||
1. cloud security groups — граница между internet/VPC/managed services;
|
||||
2. host firewall (UFW/nftables/iptables) — защита VM;
|
||||
3. `DOCKER-USER` — защита от обхода UFW опубликованными Docker ports;
|
||||
4. Docker networks — разделение сервисов внутри VM.
|
||||
|
||||
Default policy для ingress — deny. Разрешение задаёт source, destination, protocol, port и назначение. Правила «вся private network на все порты» запрещены.
|
||||
|
||||
Cloud SG для managed PostgreSQL разрешает TLS-подключения только от VM/SG сервисов, которым нужна соответствующая схема. PostgreSQL, Redis, OTLP receivers, admin UI и internal API не публикуются в интернет.
|
||||
|
||||
### Egress
|
||||
|
||||
- сервис без внешней интеграции не подключается к сети `egress`;
|
||||
- внешние destination/ports фиксируются в inventory;
|
||||
- DNS/NTP и package/image registry учитываются отдельно;
|
||||
- временный bootstrap egress удаляется при lockdown;
|
||||
- отсутствие технической возможности фильтровать по FQDN компенсируется NAT/proxy/provider firewall и мониторингом исходящих соединений.
|
||||
|
||||
### Fail2ban
|
||||
|
||||
Fail2ban обязателен для SSH, временно или постоянно доступного из интернета. Он дополняет allow-list trusted CIDR и key-only auth, а не заменяет их.
|
||||
|
||||
Для VM без публичного ingress в steady state fail2ban можно оставить включённым, но основная защита — отсутствие внешнего маршрута и закрытые SG/firewall.
|
||||
|
||||
## Минимизация host OS
|
||||
|
||||
- используется поддерживаемый минимальный образ ОС;
|
||||
- пакеты устанавливаются из доверенных репозиториев с проверкой подписи;
|
||||
- компиляторы, отладчики, сетевые утилиты и installer dependencies не остаются без эксплуатационной необходимости;
|
||||
- отключаются неиспользуемые daemon/socket units;
|
||||
- автоматические security updates или утверждённое patch window обязательны;
|
||||
- kernel и container runtime регулярно обновляются;
|
||||
- удаление пакетов выполняется по утверждённому allow-list, а не слепым `autoremove`;
|
||||
- старые images/releases удаляются только после сохранения необходимого rollback window;
|
||||
- cleanup не удаляет active image, последний рабочий релиз, forensic data или backup.
|
||||
|
||||
Для no-egress VM обновление выполняется в контролируемое окно через временный ограниченный egress либо проверенные offline packages/images. После обновления повторяется lockdown.
|
||||
|
||||
## Логи, аудит и инциденты
|
||||
|
||||
Аудируются:
|
||||
|
||||
- входы `deploy`, `admin`, `tunnel`;
|
||||
- sudo-вызовы и systemd deployment actions;
|
||||
- изменение SG/firewall/SSH/sudoers;
|
||||
- получение и ротация секретов без записи значений;
|
||||
- открытие и закрытие break-glass доступа;
|
||||
- версия/digest развернутого релиза.
|
||||
|
||||
Секреты, токены, содержимое credentials и полные PII в логи не попадают.
|
||||
|
||||
При подтверждённой компрометации контейнера:
|
||||
|
||||
1. изолировать VM/контейнер сетевыми средствами;
|
||||
2. не использовать скомпрометированную VM как доверенную точку восстановления;
|
||||
3. ротировать доступные контейнеру секреты и service tokens;
|
||||
4. проверить соседние сервисы по разрешённым network flows;
|
||||
5. сохранить необходимые snapshot/log evidence;
|
||||
6. пересоздать VM из доверенного образа вместо ручной «очистки», если затронут host;
|
||||
7. задокументировать причину выхода за границу изоляции, если он произошёл.
|
||||
|
||||
## Definition of Done для новой VM или сервиса
|
||||
|
||||
- определён класс VM: egress или private/no-egress;
|
||||
- составлена матрица ingress/egress;
|
||||
- созданы отдельные OS users и IAM principal;
|
||||
- root/password SSH отключены после проверки key access;
|
||||
- `deploy` не состоит в `docker` и имеет только конкретные systemd-команды;
|
||||
- production-файлы root-owned и недоступны `deploy` на запись;
|
||||
- каждый контейнер проверен по hardening baseline;
|
||||
- для каждого image digest проверены entrypoint, UID/GID и полный inventory
|
||||
writable paths;
|
||||
- секреты разделены по сервисам/VM и отсутствуют в обычном `.env`;
|
||||
- bind-backed secrets и staged TLS проверены по фактическим owner/mode и
|
||||
содержимому, а не только по декларации Compose;
|
||||
- runtime/migration/config-admin DB roles разделены, временные cross-schema
|
||||
grants выданы и отозваны владельцем;
|
||||
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
|
||||
updater freshness контролируется отдельным сигналом;
|
||||
- внутренние ports недоступны извне;
|
||||
- backup/restore и rollback проверены в объёме релиза;
|
||||
- для private/no-egress VM завершён и зафиксирован lockdown;
|
||||
- проверена недоступность внешних портов и неразрешённого egress;
|
||||
- отклонения имеют владельца, компенсирующую меру и срок пересмотра.
|
||||
Reference in New Issue
Block a user