Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user