Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений
This commit is contained in:
+16
-1
@@ -20,6 +20,21 @@
|
||||
| [`arch-09-redis.md`](arch-09-redis.md) | Контракт Redis: не source of truth, формат ключей, TTL, Lua, AOF/ACL. Реализация VM — `module-04-redis-vm1.md` / `module-04-redis-vm2.md` |
|
||||
| [`arch-10-deployment.md`](arch-10-deployment.md) | Контракт развёртывания: VPC/SG, PG/S3, роли `deploy`, TLS процедура, cutover. Runbook VM — `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md`. OS-роли — arch-06 |
|
||||
|
||||
## Ownership-матрица архитектурных требований
|
||||
|
||||
| Область | Канонический владелец | Что остаётся в связанных документах |
|
||||
|---|---|---|
|
||||
| Термины, поля, enum и базовый lifecycle | [`arch-00-glossary.md`](arch-00-glossary.md) | Сценарии переходов ссылаются на arch-00 и не переопределяют значения |
|
||||
| Границы сервисов и пользовательские сценарии | [`arch-01-system-architecture.md`](arch-01-system-architecture.md) | API, Compose и deployment описывают реализацию этих границ |
|
||||
| HTTP/API/realtime контракты | [`arch-02-api-contracts.md`](arch-02-api-contracts.md) | Routing и rollout только ссылаются на endpoint/auth contract |
|
||||
| Compose, Docker networks, mounts и published ports | [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | arch-06 задаёт security baseline; arch-08 — поведение nginx |
|
||||
| Non-secret env, secret references и business settings | [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | Модули задают schema/validation конкретного потребителя |
|
||||
| OS-роли, secret delivery и host/container hardening | [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md) | arch-03 применяет требования в Compose; arch-10 ставит rollout gates |
|
||||
| Observability contract | [`arch-07-observability.md`](arch-07-observability.md) | VM module-09 задаёт конкретные pipelines/alerts |
|
||||
| Nginx, TLS/ACME, единый `308`, request id и reload | [`arch-08-nginx.md`](arch-08-nginx.md) | arch-03 задаёт mounts/ports и сохраняет исключение HTTP webhook ВМ2; VM module-03 — routing matrix |
|
||||
| Redis contract | [`arch-09-redis.md`](arch-09-redis.md) | VM module-04 задаёт конкретную карту instances/keys |
|
||||
| Provisioning, rollout/cutover, backup/rollback/DR | [`arch-10-deployment.md`](arch-10-deployment.md) | Профильный module-10 содержит исполняемые команды и VM-specific gates |
|
||||
|
||||
## Как читать
|
||||
|
||||
1. Начните с **arch-01** — общая картина и зафиксированные решения MVP.
|
||||
@@ -61,7 +76,7 @@
|
||||
|
||||
| Тема | Где зафиксировано |
|
||||
|---|---|
|
||||
| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» |
|
||||
| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | Спецификация: [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md); API-контракты — [`arch-02-api-contracts.md`](arch-02-api-contracts.md) |
|
||||
| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | Спецификация: [`module-11-idgtl-sms.md`](../VM1_app/documentation/module-11-idgtl-sms.md) (доставка через Direct SMS API; проверка OTP — локально в Keycloak) |
|
||||
|
||||
## Каноническое размещение production-контуров
|
||||
|
||||
@@ -126,12 +126,14 @@
|
||||
|
||||
| Значение | Когда |
|
||||
|---|---|
|
||||
| `accepted` | Сообщение принято API, safety ещё не завершена или только начата |
|
||||
| `processing` | Внутренний/transient на время sync-wait safety; клиенту на `POST .../messages` не отдаётся как финальный ответ |
|
||||
| `accepted` | Получен финальный `allow`, сообщение и durable-намерение доставки зафиксированы, но Open Lines ещё не подтвердил приём |
|
||||
| `processing` | Сообщение принято API, Safety ещё выполняется (включая sync-wait `202 pending`); клиенту на `POST .../messages` не отдаётся как финальный ответ |
|
||||
| `delivered` | Финальный `allow`, сообщение ушло в Open Lines (или входящее от оператора сохранено) |
|
||||
| `rejected` | Финальный `deny` от Message Safety |
|
||||
| `failed` | Инфраструктурная ошибка доставки (Bitrix/S3), не safety-deny |
|
||||
|
||||
Для исходящего сообщения успешный lifecycle строго следует порядку: Safety `allow` → `accepted` → приём в Open Lines → `delivered`. Статус `accepted` не означает незавершённую Safety-проверку.
|
||||
|
||||
Realtime-событие `message.status` передаёт актуальные `safety_status` и/или `delivery_status`.
|
||||
|
||||
## `MessageAttachment.scan_status`
|
||||
|
||||
@@ -195,7 +195,7 @@ Frontend не должен:
|
||||
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
|
||||
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
|
||||
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
|
||||
- хранение согласий пользователя в App DB (**`user_id`**, **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT;
|
||||
- хранение согласий пользователя в App DB (**`user_id`**, nullable **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT; при bootstrap `ux_session_id=NULL` допустим, потому что новая UX-сессия создаётся следующим запросом;
|
||||
- профиль, структурированный блоками;
|
||||
- API чата, истории, файлов и документов;
|
||||
- realtime-доставку входящих сообщений клиенту;
|
||||
@@ -435,7 +435,7 @@ api-backend не решает, sync или async нужна проверка в
|
||||
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`**: значение сверяется локально с HMAC OTP, сгенерированного Keycloak и переданного в закрытом заказе `sms-service`; статусы Direct и callback на verify не влияют.
|
||||
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
|
||||
12. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
|
||||
13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` → минимальный профиль.
|
||||
13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` с nullable `ux_session_id` (на bootstrap обычно `NULL`) → минимальный профиль.
|
||||
14. Frontend вызывает **`POST /api/v1/analytics/session-start`** (если нужна новая UX-сессия) и далее работает с `X-Ux-Session-Id`.
|
||||
15. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
|
||||
16. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
|
||||
@@ -596,56 +596,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
||||
|
||||
### Предлагаемая структура backend-репозитория
|
||||
|
||||
```text
|
||||
backend/
|
||||
docker-compose.yml # root compose ВМ1
|
||||
.env.example
|
||||
nginx/
|
||||
docker-compose.yml
|
||||
nginx.conf
|
||||
conf.d/
|
||||
certs/
|
||||
.gitkeep
|
||||
api-backend/
|
||||
app/
|
||||
docker-compose.yml
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
bitrix-local-app/
|
||||
app/
|
||||
docker-compose.yml
|
||||
deploy/
|
||||
tests/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
keycloak/
|
||||
docker-compose.yml
|
||||
realm/
|
||||
themes/
|
||||
providers/
|
||||
sms-service/
|
||||
app/
|
||||
migrations/
|
||||
openapi.yaml
|
||||
Dockerfile
|
||||
redis/
|
||||
docker-compose.yml
|
||||
observability/
|
||||
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).
|
||||
Каноническая структура root Compose, service includes, networks и mounts задаётся в [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Детальная внутренняя структура сервиса определяется его профильной спецификацией.
|
||||
|
||||
### Compose-контуры
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ networks:
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
nginx-certs:
|
||||
```
|
||||
|
||||
Root Compose ВМ2 включает собственный nginx с public/private server blocks, Message Safety API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety и локальный OTEL Collector. Секреты, сети и volumes двух projects не общие.
|
||||
@@ -103,6 +102,7 @@ Root Compose ВМ2 включает собственный nginx с public/priva
|
||||
- Публикация портов наружу (`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, но в составе корневого контура они переопределяются общими.
|
||||
- При сборке образов нужно добавлять защиту на CRLF → LF
|
||||
|
||||
### Обязательный container hardening
|
||||
|
||||
@@ -176,7 +176,7 @@ docker compose exec api-backend ruff format .
|
||||
|
||||
Требования:
|
||||
|
||||
- публикует наружу только `80` и `443` (см. политику HTTP ниже);
|
||||
- публикует в internet только `80` и `443` (см. политику HTTP ниже); private `8443` ВМ2 публикуется только в VPC/SG для ВМ1 и ops;
|
||||
- принимает внешний HTTPS-трафик;
|
||||
- выполняет TLS termination на reverse proxy; внутренний HTTP между контейнерами — только в закрытой Docker-сети `backend`;
|
||||
- non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`,
|
||||
@@ -196,13 +196,7 @@ docker compose exec api-backend ruff format .
|
||||
- при recreate/смене IP upstream действует та же post-ready reload policy либо
|
||||
используется явно протестированный dynamic resolver; stale IP/DNS в
|
||||
загруженной nginx config недопустим;
|
||||
- **политика 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-домену недоступны;
|
||||
- **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS;
|
||||
- **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host);
|
||||
- **Bitrix local-app callbacks ВМ1** (`/bitrix/handler|install|placement`): только HTTPS; порт `80` — только redirect;
|
||||
- **CRM sync webhook ВМ2** (exact `/bitrix/sync/webhook/contact|alert`): только HTTPS на отдельном processing host; HTTP не отражает query token в redirect;
|
||||
- применяет каноническую HTTP/HTTPS policy из [`arch-08-nginx.md`](arch-08-nginx.md): web/единый MVP host использует только `308` на HTTPS, выделенный API host не слушает `:80`; единственное route-specific исключение — HTTP exact CRM webhook ВМ2 возвращает `404/426` без redirect и без отражения query token;
|
||||
- маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
|
||||
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
|
||||
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
|
||||
@@ -415,16 +409,19 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
||||
|
||||
## Volumes
|
||||
|
||||
Минимальные volumes:
|
||||
Минимальные persistent volumes:
|
||||
|
||||
- ВМ1: Redis DB0/DB1 data, public TLS/ACME, local OTEL queue;
|
||||
- ВМ1: Redis DB0/DB1 data, 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.
|
||||
Public TLS и ACME на обеих VM — не named volumes. Root-only ACME state остаётся
|
||||
на host в `/etc/letsencrypt`; root hook атомарно копирует только нужные
|
||||
`fullchain.pem`/`privkey.pem` в `/var/lib/han-chat/public-tls`. Compose
|
||||
монтирует этот staging read-only в nginx, а host webroot
|
||||
`/var/lib/han-chat/acme` — в nginx и ACME client с минимально необходимыми
|
||||
правами. Internal TLS certificate/key передаются отдельными Compose secrets и
|
||||
не объединяются с public TLS.
|
||||
|
||||
Данные PostgreSQL **не** хранятся в Docker volumes — только managed PostgreSQL вне compose.
|
||||
|
||||
@@ -454,9 +451,9 @@ Local OTEL queue на каждой VM использует отдельный pe
|
||||
|
||||
| Host | Порт 80 | Порт 443 | Примечание |
|
||||
|---|---|---|---|
|
||||
| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host |
|
||||
| Веб-домен (frontend) | только `308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host |
|
||||
| API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` |
|
||||
| Bitrix Local App ВМ1 (`/bitrix/handler|install|placement`) | только redirect на web host | HTTPS | install/handler/placement |
|
||||
| Bitrix Local App ВМ1 (`/bitrix/handler|install|placement`) | только `308` на HTTPS | HTTPS | install/handler/placement |
|
||||
| CRM webhook ВМ2 (exact `/bitrix/sync/webhook/contact|alert`) | generic `404/426`, без redirect query token | HTTPS | отдельный processing host |
|
||||
|
||||
Правила:
|
||||
@@ -468,26 +465,12 @@ Local OTEL queue на каждой VM использует отдельный pe
|
||||
|
||||
### TLS и заголовки
|
||||
|
||||
- cookies в web-клиенте: `Secure`, `HttpOnly`, корректный `SameSite`;
|
||||
- OIDC redirect URI в Keycloak — HTTPS;
|
||||
- `KEYCLOAK_PUBLIC_URL`, issuer и frontend auth discovery URL совпадают по схеме, host и path;
|
||||
- backend формирует внешние ссылки с учётом `X-Forwarded-Proto=https`;
|
||||
- HSTS включается в production-like среде **после** проверки доменов и сертификатов;
|
||||
- TLS 1.0/1.1 запрещены; минимум TLS 1.2, предпочтительно TLS 1.3;
|
||||
- слабые шифры запрещены на уровне `nginx`;
|
||||
- `nginx` скрывает `Server`, `X-Powered-By` и аналогичные технологические заголовки;
|
||||
- security headers: `Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy`, `Content-Security-Policy` для web-приложения;
|
||||
- инструкция по установке всегда открывается новой вкладкой, поэтому 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 и совпадение ключа;
|
||||
- закрыть прямой доступ к внутренним портам контейнеров извне.
|
||||
TLS versions, trusted CA, HSTS/security headers, certificate validation и safe
|
||||
reload принадлежат [`arch-08-nginx.md`](arch-08-nginx.md); cookie/OIDC и
|
||||
application security — [`arch-01-system-architecture.md`](arch-01-system-architecture.md).
|
||||
Compose применяет их через host binds из раздела «Volumes», не монтирует
|
||||
root-only `/etc/letsencrypt` в nginx, не объявляет named volume public
|
||||
certificates и не публикует внутренние порты.
|
||||
|
||||
## Nginx routing для Bitrix24 Local App
|
||||
|
||||
@@ -495,7 +478,7 @@ Local OTEL queue на каждой VM использует отдельный pe
|
||||
|
||||
Рекомендуемая схема:
|
||||
|
||||
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
|
||||
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → `308` HTTPS; `:443` — TLS + маршрутизация;
|
||||
- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`;
|
||||
- для `location` WebSocket (`/api/v1/realtime`): `proxy_http_version 1.1`, `Upgrade`/`Connection` headers, увеличенный `proxy_read_timeout`;
|
||||
- только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` на ВМ1 → `bitrix-local-app`; CRM `/bitrix/sync/*` на этом host не маршрутизируется;
|
||||
@@ -576,7 +559,7 @@ WAF не заменяет обязательные лимиты, валидац
|
||||
|
||||
Минимальные проверки:
|
||||
|
||||
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
|
||||
- `nginx`: на веб-домене — `308` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — успешная TLS handshake и ожидаемый route response;
|
||||
- `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`;
|
||||
@@ -635,7 +618,7 @@ endpoint обязан fail-closed при недоступной требуемо
|
||||
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.
|
||||
4. Public nginx ВМ2 публикует `80/443`; `80` обслуживает ACME, а HTTP exact CRM webhook возвращает `404/426` без 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` запрещён.
|
||||
|
||||
Host firewall обеих VM учитывает post-DNAT semantics `DOCKER-USER`: policy
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
- Для часто используемых фильтров добавляются индексы.
|
||||
- Миграции не должны удалять данные без отдельного согласования.
|
||||
- Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют.
|
||||
|
||||
- При создании миграций учитывать, что asyncpg допускает один top-level SQL statement на один execute.
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -525,15 +525,15 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
### Обнаруженные архитектурные конфликты
|
||||
|
||||
1. `arch-03` разрешает stdout/platform exporter как минимум, но production-like расследования и alerts без backend ограничены. Закрыто выбором SigNoz (O1 / O-TBD1); stdout остаётся аварийным buffer.
|
||||
2. `module-05` использует test-only terminal `400` и non-sticky verdict вместо canonical `403`/sticky production verdict. Dashboards обязаны маркировать сервис `stub`; production SLO Safety на нём недостоверен. Детали — спецификация ВМ2.
|
||||
3. `module-07` задаёт full sync target; до code/portal cutover dashboard обязан показывать `sync_disabled`, а не синтетический CRM success. Queue/CRM SLI включаются только после module-07 preflight. Детали — спецификация ВМ2.
|
||||
2. Test-only режимы Message Safety не входят в production SLO. Production dashboards используют canonical `403`, sticky verdict и фактический `processing_mode`; значение `stub` считается ошибкой cutover. Детали — спецификация ВМ2.
|
||||
3. Queue/CRM SLI включаются только после module-07 preflight/cutover и отражают фактическое состояние `bitrix-sync` на ВМ2; синтетический CRM success запрещён. Детали — спецификация ВМ2.
|
||||
4. Retention, RPO/RTO и production SLO открыты в module-01/04/06/08; значения этого документа являются initial ops policy, не закрывают legal/product TBD.
|
||||
5. Observability env (`OTEL_REMOTE_*`, service names, sampling/queue limits) добавлены в arch-04 и `.env.example`; sampling/queue limits уточняются после load test.
|
||||
|
||||
## 19. Ссылки
|
||||
|
||||
- Профильные спецификации: [`module-09-observability-vm1.md`](../VM1_app/documentation/module-09-observability-vm1.md), [`module-09-observability-vm2.md`](../VM2_services/documentation/module-09-observability-vm2.md).
|
||||
- VM/Docker logging и firewall: [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh).
|
||||
- Прототипный nginx stdout/access log: [`../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf).
|
||||
- VM/Docker logging и firewall: [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md) и профильные module-10/runbook.
|
||||
- Nginx stdout/access log: [`arch-08-nginx.md`](arch-08-nginx.md) и профильные module-03.
|
||||
- HTTP/OTLP registry: [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||
- Compose topology: [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
|
||||
|
||||
@@ -40,7 +40,8 @@ Upstream failures не перенаправляются на другой сер
|
||||
|
||||
## 3. HTTP/HTTPS и TLS
|
||||
|
||||
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`, кроме явно зафиксированных исключений профильной спецификации;
|
||||
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`;
|
||||
- единственное route-specific исключение: exact CRM webhook ВМ2 по HTTP возвращает `404/426` без redirect, чтобы query token не отражался в `Location`;
|
||||
- выделенный API host, если появится, не имеет listener `:80`;
|
||||
- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy;
|
||||
- сертификат доверенного CA, private key read-only и недоступен приложению;
|
||||
@@ -53,24 +54,33 @@ Upstream failures не перенаправляются на другой сер
|
||||
|
||||
## 4. ACME lifecycle
|
||||
|
||||
Выбран webroot Certbot/ACME client с общими named volumes:
|
||||
Выбран webroot Certbot/ACME client с host-каталогами:
|
||||
|
||||
```text
|
||||
nginx-certs -> /etc/letsencrypt (rw у certbot, ro у nginx)
|
||||
nginx-acme-webroot -> /var/www/certbot
|
||||
/etc/letsencrypt root-only ACME state; rw только у root/ACME client
|
||||
/var/lib/han-chat/public-tls staged fullchain.pem/privkey.pem; ro bind в nginx
|
||||
/var/lib/han-chat/acme webroot; bind в nginx и ACME client
|
||||
```
|
||||
|
||||
Каждая VM выпускает **свой** сертификат на свой public host. Секреты и volumes двух projects не общие.
|
||||
Named volume для public certificate/key запрещён. Non-root nginx никогда не
|
||||
монтирует `/etc/letsencrypt`: после успешного issuance/renewal root hook
|
||||
проверяет certificate/key и атомарно копирует только нужные PEM в
|
||||
`/var/lib/han-chat/public-tls` с `root:<dedicated-tls-group>`, directory `0750`
|
||||
и files `0640`. Каждая VM выпускает **свой** сертификат на свой public host;
|
||||
host-каталоги двух VM не общие.
|
||||
|
||||
Bootstrap:
|
||||
|
||||
1. DNS указывает на VM; 80/443 разрешены.
|
||||
2. Запустить временный HTTP config с `/.well-known/acme-challenge/`.
|
||||
3. Выпустить certificate без остановки nginx.
|
||||
4. Проверить `nginx -t`, атомарно активировать TLS config, reload.
|
||||
2. Подготовить root-owned webroot `/var/lib/han-chat/acme` и временный HTTP config с `/.well-known/acme-challenge/`.
|
||||
3. Выпустить certificate в root-only `/etc/letsencrypt` без остановки nginx.
|
||||
4. Проверить certificate/key, атомарно обновить `/var/lib/han-chat/public-tls`, выполнить `nginx -t`, активировать TLS config и reload.
|
||||
|
||||
Renew container/host timer выполняет `certbot renew` минимум дважды в сутки;
|
||||
после фактического renewal проверяет рабочую конфигурацию командой
|
||||
Root-owned host timer выполняет `certbot renew` минимум дважды в сутки; ACME
|
||||
client может быть контейнеризован, но только он получает rw bind
|
||||
`/etc/letsencrypt`, а public certificate остаётся host bind, не named volume.
|
||||
После фактического renewal hook проверяет пару certificate/key, атомарно
|
||||
обновляет staging и проверяет рабочую конфигурацию командой
|
||||
`docker compose exec -T nginx nginx -t -c /tmp/nginx.conf` и отправляет
|
||||
master-процессу `docker compose kill -s HUP nginx`. Bare-команды `nginx -t` и
|
||||
`nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся
|
||||
@@ -82,7 +92,8 @@ success path считается дефектом интеграции с Certbot
|
||||
Контролируются expiry days и последняя успешная попытка. Staging CA используется
|
||||
в rehearsal, чтобы не исчерпать лимиты.
|
||||
|
||||
Non-root nginx не монтирует root-only дерево Let's Encrypt целиком: только необходимые cert/key files по arch-03/arch-06.
|
||||
Non-root nginx получает только read-only staging
|
||||
`/var/lib/han-chat/public-tls`; root-only `/etc/letsencrypt` ему недоступен.
|
||||
|
||||
## 5. Request ID и forwarded headers
|
||||
|
||||
@@ -176,7 +187,7 @@ nginx/
|
||||
|
||||
## 11. Docker Compose
|
||||
|
||||
`nginx` подключён к `public` и `backend` (или эквивалентным сетям своей VM). Публикация host ports разрешена только nginx. Filesystem read-only, tmpfs для cache/run/temp/`/etc/nginx/conf.d` по arch-03, non-root где позволяет bind ports/capabilities. Cert volumes read-only для nginx. ACME client имеет только необходимые volumes/network.
|
||||
`nginx` подключён к `public` и `backend` (или эквивалентным сетям своей VM). Публикация host ports разрешена только nginx. Filesystem read-only, tmpfs для cache/run/temp/`/etc/nginx/conf.d` по arch-03, non-root где позволяет bind ports/capabilities. Host staging public TLS и ACME webroot монтируются с минимальными правами; named volume сертификатов запрещён. ACME client имеет только необходимые bind mounts/network.
|
||||
|
||||
Non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`, `/var/cache/nginx`, `/var/run` и `/tmp`; tmpfs задаёт явные UID/GID/mode. Основной `nginx.conf` подключает конкретный rendered include, не неограниченный wildcard, который позволил бы обойти `nginx -t`.
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
|
||||
7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
|
||||
8. Миграции выполняются отдельными one-shot steps до новой версии приложения.
|
||||
9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
|
||||
10. `bitrix-sync` вводится только после выполнения preflight/cutover gates module-07; до этого `BITRIX_SYNC_ENABLED=false`, public webhook закрыт на edge.
|
||||
9. Production Message Safety работает только на ВМ2; local stub/fallback на ВМ1 запрещён. Caller ВМ1 использует private HTTPS, service token и проверку internal CA, при недоступности — fail closed.
|
||||
10. `bitrix-sync` работает только на ВМ2 и вводится после preflight/cutover gates module-07; до подтверждённого cutover public webhook остаётся закрыт на edge.
|
||||
11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо.
|
||||
12. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
|
||||
13. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06.
|
||||
@@ -49,7 +49,7 @@
|
||||
<VM_PRIVATE_IP> приватный IPv4 VM
|
||||
<VPC_CIDR> например 10.20.0.0/24
|
||||
<PG_PRIVATE_HOST> private FQDN/IP managed PG
|
||||
<PG_PORT> 5432 или 6432
|
||||
<PG_PORT> 5433 для Selectel PgBouncer; иной порт — только фактическое значение другого provider/endpoint
|
||||
<PG_DATABASE> han_chat
|
||||
<BACKEND_REPO_URL> URL репозитория этой VM
|
||||
<BACKEND_ROOT> /opt/han-chat/backend
|
||||
@@ -115,7 +115,7 @@ DNS: `A <PUBLIC_HOST> → <VM1_PUBLIC_IP>`, `A <PROCESSING_PUBLIC_HOST> → <VM2
|
||||
|
||||
## 5. Stage 2 — hardening Ubuntu
|
||||
|
||||
Процедура первичная для **каждой** application VM. Скрипт-прототип [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh) полезен для UFW, fail2ban, Docker и `DOCKER-USER`. Перед production: review версии; не передавать IP/ключи в git; проверить unattended upgrades; `deploy` не в группе `docker`; `AllowTcpForwarding no` по умолчанию; отдельный `tunnel` при необходимости; root-owned systemd-units и `/etc/sudoers.d/deploy` без wildcard.
|
||||
Процедура первичная для **каждой** application VM и выполняется только по её профильному production runbook. Перед production: review версии; не передавать IP/ключи в git; проверить unattended upgrades; `deploy` не в группе `docker`; `AllowTcpForwarding no` по умолчанию; отдельный `tunnel` при необходимости; root-owned systemd-units и `/etc/sudoers.d/deploy` без wildcard.
|
||||
|
||||
`PUBLIC_DOCKER_PORTS` задаёт профильный runbook (ВМ1: `80,443`; ВМ2 — свои public 80/443, private 8443 не internet).
|
||||
|
||||
@@ -216,37 +216,24 @@ Validation: нет `change-me`, paired tokens equal, PG TLS, public HTTPS, `FRON
|
||||
|
||||
## 9. TLS/ACME процедура
|
||||
|
||||
Webroot two-phase, независимо в root Compose каждой VM, без `compose down`. Staging CA rehearsal, затем production `--cert-name` текущего host. Сертификат и ACME volume между VM не разделяются.
|
||||
Канонические bootstrap, renewal, validation и safe reload задаёт
|
||||
[`arch-08-nginx.md`](arch-08-nginx.md). Rollout каждой VM применяет эту модель
|
||||
независимо, без `compose down` и без named volume сертификатов:
|
||||
|
||||
Renew: systemd timer дважды в сутки. Скрипт `<BACKEND_ROOT>/deploy/ssl-renew.sh`:
|
||||
- root-only ACME state: `/etc/letsencrypt`;
|
||||
- read-only для nginx host staging: `/var/lib/han-chat/public-tls`;
|
||||
- host ACME webroot: `/var/lib/han-chat/acme`;
|
||||
- root-owned systemd timer/hook дважды в сутки с `flock`; пользователь `deploy`
|
||||
может запускать только утверждённый unit и не получает доступ к ACME state;
|
||||
- staging CA rehearsal предшествует production issuance; после renewal root
|
||||
hook проверяет certificate/key, атомарно обновляет staging, выполняет
|
||||
container `nginx -t` и только затем HUP;
|
||||
- success path возвращает `0` с пустым stderr; ошибка сохраняет действующий
|
||||
certificate/config и поднимает alert (<21 дней, page <7 дней).
|
||||
|
||||
1. взять `flock`;
|
||||
2. `docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet`;
|
||||
3. при обновлении проверить `docker compose exec -T nginx nginx -t -c /tmp/nginx.conf`;
|
||||
4. только после успеха `docker compose kill -s HUP nginx`;
|
||||
5. записать результат и метрику expiry;
|
||||
6. ненулевой exit при ошибке;
|
||||
7. не удалять действующий сертификат;
|
||||
8. success path — `0` и пустой stderr.
|
||||
|
||||
Пример unit `/etc/systemd/system/han-chat-cert-renew.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Renew HAN Chat Let's Encrypt certificate
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=deploy
|
||||
WorkingDirectory=<BACKEND_ROOT>
|
||||
ExecStart=<BACKEND_ROOT>/deploy/ssl-renew.sh
|
||||
```
|
||||
|
||||
Timer `OnCalendar=*-*-* 03,15:20:00`, `RandomizedDelaySec=30m`, `Persistent=true`. `enable --now`, `list-timers`, `certbot renew --dry-run`. Alert <21 дней, page <7 дней. Ошибка renew не останавливает nginx.
|
||||
|
||||
Staging issuance, затем production `--cert-name` текущего host. Host для ВМ1 — `<PUBLIC_HOST>`; для ВМ2 — `<PROCESSING_PUBLIC_HOST>`; private `8443` — internal CA, не Let's Encrypt. Сертификат и ACME volume между VM не разделяются.
|
||||
Host ВМ1 — `<PUBLIC_HOST>`, ВМ2 — `<PROCESSING_PUBLIC_HOST>`; private `8443`
|
||||
использует internal CA, не Let's Encrypt. Ни ACME state, ни staging между VM не
|
||||
разделяются.
|
||||
|
||||
## 10. Сквозной порядок startup и cutover
|
||||
|
||||
@@ -337,4 +324,4 @@ DR потеря VM: новая Ubuntu в VPC, hardening, DNS, secrets из vault
|
||||
|
||||
- ВМ1: [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md).
|
||||
- ВМ2: [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md).
|
||||
- Прототипы: [`../../HAN_chat/Deploy_steps.md`](../../HAN_chat/Deploy_steps.md), [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py).
|
||||
- Исполняемые процедуры: [`RUNBOOK.production.ru.md`](../VM1_app/codebase/backend/deployment/RUNBOOK.production.ru.md) для ВМ1 и [`RUNBOOK.ru.md`](../VM2_services/codebase/services/deployment/RUNBOOK.ru.md) для ВМ2.
|
||||
|
||||
Reference in New Issue
Block a user