Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений

This commit is contained in:
mi
2026-08-19 18:24:00 +03:00
parent bbef7a30c9
commit c7a80e7256
103 changed files with 3457 additions and 3725 deletions
+16 -1
View File
@@ -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-контуров
+4 -2
View File
@@ -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`
+3 -52
View File
@@ -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
+4 -4
View File
@@ -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).
+23 -12
View File
@@ -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`.
+21 -34
View File
@@ -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.