Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
+226 -82
View File
@@ -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