630 lines
52 KiB
Markdown
630 lines
52 KiB
Markdown
# 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). VM/SSH, OS-роли, секреты, systemd-деплой и hardening — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||
|
||
## Назначение
|
||
|
||
Этот документ описывает целевой Docker Compose контур для первой production-like среды. Он не заменяет будущий `docker-compose.yml`, но задает требования, которым он должен соответствовать.
|
||
|
||
Требования к безопасности на уровне приложения и данных — в [`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`.
|
||
|
||
## Один root Compose project на каждую VM (обязательно)
|
||
|
||
Это зафиксированное архитектурное требование, а не рекомендация.
|
||
|
||
### Принцип независимого входа по VM
|
||
|
||
- ВМ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`;
|
||
- exact `POST /callbacks/idgtl/sms` → `sms-service`; остальные методы и SMS paths не публикуются;
|
||
- web-сборка frontend или прокси на dev-сервер;
|
||
- `/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`
|
||
|
||
Каждый сервис описывается в собственном `docker-compose.yml` и подключается в root-файл своей VM директивой `include`:
|
||
|
||
```text
|
||
backend/
|
||
docker-compose.yml # root ВМ1
|
||
.env
|
||
nginx/
|
||
docker-compose.yml # описание сервиса nginx (или секция в корневом)
|
||
nginx.conf
|
||
conf.d/
|
||
certs/
|
||
.gitkeep
|
||
api-backend/
|
||
docker-compose.yml # описание сервиса api-backend
|
||
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` (принципиальная схема):
|
||
|
||
```yaml
|
||
name: han-chat
|
||
|
||
include:
|
||
- nginx/docker-compose.yml
|
||
- api-backend/docker-compose.yml
|
||
- bitrix-local-app/docker-compose.yml
|
||
- keycloak/docker-compose.yml
|
||
- sms-service/docker-compose.yml
|
||
- redis/docker-compose.yml
|
||
- observability/docker-compose.yml
|
||
|
||
networks:
|
||
public:
|
||
backend:
|
||
egress:
|
||
observability:
|
||
|
||
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 не общие.
|
||
|
||
### Правила для сервисных compose-файлов
|
||
|
||
- Сервисный `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
|
||
docker compose up -d
|
||
docker compose logs -f nginx
|
||
docker compose logs -f api-backend
|
||
docker compose logs -f message-safety
|
||
docker compose logs -f bitrix-sync
|
||
docker compose logs -f bitrix-local-app
|
||
docker compose exec api-backend alembic upgrade head
|
||
docker compose exec api-backend pytest
|
||
docker compose exec api-backend ruff check .
|
||
docker compose exec api-backend ruff format .
|
||
```
|
||
|
||
## Сервисы
|
||
|
||
### nginx ВМ1 и ВМ2
|
||
|
||
На каждой 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-домену недоступны;
|
||
- **единый домен 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 callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect;
|
||
- маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
|
||
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
|
||
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
|
||
- 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` наружу;
|
||
- **production-like / production**: отдаёт **статическую сборку Expo web** из volume или каталога (`/usr/share/nginx/html` или аналог); `index.html` + assets, SPA fallback `try_files $uri /index.html`;
|
||
- **local dev** (опционально): при `FRONTEND_DEV_PROXY_ENABLED=true` проксирует `/` на Expo dev server (`EXPO_DEV_SERVER_URL`, напр. `http://host.docker.internal:8081`);
|
||
- передает upstream-сервисам `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`;
|
||
- если входящий запрос **без** `X-Request-ID`, nginx **генерирует** UUID и устанавливает заголовок до proxy_pass (I3);
|
||
- задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`;
|
||
- для `POST /api/v1/dialogs/*/messages` `proxy_read_timeout` должен быть не меньше `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s`, чтобы nginx не обрывал sync-wait при async file scan;
|
||
- применяет edge rate limits для auth, API и download endpoints;
|
||
- ограничивает частоту соединений и размер тела запроса;
|
||
- разрешает только TLS 1.2/1.3 и запрещает слабые шифры;
|
||
- добавляет HSTS и базовые security headers;
|
||
- скрывает заголовки, раскрывающие внутренние технологии;
|
||
- кэширует публичные endpoint настроек и контента;
|
||
- не проксирует наружу managed PostgreSQL, `redis`, `otel-collector` (БД вне compose, в VPC);
|
||
|
||
### api-backend
|
||
|
||
Python FastAPI backend.
|
||
|
||
Требования:
|
||
|
||
- запускается после доступности managed PostgreSQL, `keycloak`, `redis`;
|
||
- применяет non-secret настройки из `.env` и runtime secrets из явно смонтированных secret files;
|
||
- отдает `/health/live` и `/health/ready`;
|
||
- корректно работает за reverse proxy и доверяет proxy headers только от `nginx`;
|
||
- применяет API-level rate limits с состоянием в Redis;
|
||
- вызывает message safety pipeline для сообщений до отправки в Open Lines;
|
||
- вызывает `bitrix-local-app` для отправки сообщений в Open Lines;
|
||
- принимает forward нормализованных событий оператора от `bitrix-local-app`;
|
||
- поддерживает realtime endpoint для сообщений оператора;
|
||
- работает с Selectel S3 для файлов и документов;
|
||
- обслуживает Notification Center G/P и Internal Create/Cancel; token каждого продюсера передаётся через env/secret, в App DB хранится только hash;
|
||
- имеет отдельные процессы expire job (ежедневно 00:01 UTC, advisory lock) и cleanup upload drafts/S3; они используют тот же immutable image и не публикуют порты;
|
||
- экспортирует traces/logs в `otel-collector`;
|
||
- не хранит состояние внутри контейнера.
|
||
|
||
### message-safety
|
||
|
||
Отдельный сервис ВМ2 для проверки исходящих сообщений пользователя. HTTP-контракт — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety».
|
||
|
||
Требования:
|
||
|
||
- запускается после доступности 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;
|
||
- 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 API/worker service ВМ2 для durable двусторонней синхронизации App DB ↔ Bitrix24 CRM. Каноническая постановка — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md).
|
||
|
||
Требования:
|
||
|
||
- запускается при доступном managed PostgreSQL; Redis не является зависимостью sync;
|
||
- читает задачи из `han_app.sync_queue` (заполняется триггерами App DB);
|
||
- владеет схемой `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
|
||
|
||
Локальное приложение Bitrix24 и custom connector `han_mobile_app`.
|
||
|
||
Требования:
|
||
|
||
- публикует наружу только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/live`, `/health/ready`;
|
||
- принимает `ONAPPINSTALL` и `ONIMCONNECTOR*` события от Bitrix24;
|
||
- регистрирует и активирует connector `han_mobile_app` для открытой линии 8;
|
||
- хранит OAuth-токены Bitrix24, inbox событий и `dialog_sessions` в managed PostgreSQL, схема `bitrix_local`;
|
||
- предоставляет internal API `POST /internal/openlines/v1/messages` и `GET /internal/openlines/v1/dialogs/{external_chat_id}` для api-backend;
|
||
- защищает internal API через `Authorization: Bearer {BITRIX_INTERNAL_API_TOKEN}`;
|
||
- forward-ит нормализованные события Open Lines в API, если задан `BITRIX_API_FORWARD_URL`;
|
||
- не хранит бизнес-данные приложения и не пишет напрямую в App DB.
|
||
|
||
### Managed PostgreSQL
|
||
|
||
**Во всех средах** (production, production-like, local dev) данные хранятся в **managed PostgreSQL** провайдера. Контейнер PostgreSQL в Docker Compose **не используется** — ни для production, ни для локальной разработки.
|
||
|
||
Прикладные данные, Keycloak, `bitrix-sync`, `bitrix-local-app` и `message-safety` подключаются к одной managed базе по URL из `.env` (`HAN_PG_HOST`, `HAN_PG_PORT`, `HAN_PG_DATABASE` и схемо-специфичные `*_DATABASE_URL`).
|
||
|
||
Требования:
|
||
|
||
- подключение только из приватной сети VPC (VM → managed PostgreSQL);
|
||
- одна managed база: схемы `han_app`, `bitrix_sync`, `message_safety`, `bitrix_local`, `keycloak`, `sms`;
|
||
- отдельные 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 выполняются отдельным 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
|
||
|
||
Identity provider. **Обязателен** в compose-контуре с первого запуска.
|
||
|
||
Требования:
|
||
|
||
- отдельный realm для приложения;
|
||
- отдельный frontend client с PKCE (обязателен);
|
||
- confidential backend client — **optional** (не используется в hot path MVP; S2S между сервисами — service tokens);
|
||
- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению;
|
||
- включены proxy settings для работы за `nginx`;
|
||
- импорт realm в local/dev;
|
||
- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше);
|
||
- OTP mock / SMS SPI — см. arch-04; real mode вызывает `sms-service` по сети `backend`, а единственный утверждённый внешний вызов Keycloak через `egress` — server-side validation Yandex SmartCaptcha;
|
||
- healthcheck;
|
||
- взаимодействия — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Frontend ↔ Keycloak», и [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Keycloak».
|
||
|
||
### sms-service и sms-worker
|
||
|
||
- `sms-service`: networks `backend`, `observability` и `egress` только если тот же process принимает callback и выполняет worker; `expose: 8080`, без host `ports`.
|
||
- При отдельном `sms-worker`: networks только `egress`, `observability` и доступ к managed PG; HTTP port не exposed/published.
|
||
- Оба используют `SMS_DATABASE_URL` к schema `sms`; только worker получает `IDGTL_SMS_API_KEY`.
|
||
- Callback credentials получает receiver для проверки и worker для формирования callback URL; Keycloak получает только `KEYCLOAK_SMS_SERVICE_TOKEN`.
|
||
- `sms-service` применяет собственные versioned migrations/seed; DDL-on-start запрещён. Readiness проверяет DB/schema, active approved `auth_otp` template, sender и API-key configuration.
|
||
- Ожидание Direct до 70 секунд происходит только в worker. `uncertain` не retry-ится автоматически; provider outage не создаёт restart loop и не отменяет active Keycloak challenge.
|
||
|
||
### redis
|
||
|
||
Кэш, rate limiting, coordination (не единственное хранилище бизнес-событий).
|
||
|
||
Требования:
|
||
|
||
- не использовать как единственное надежное хранилище бизнес-событий;
|
||
- хранить счетчики API-level rate limits и idempotency keys (`api-backend`);
|
||
- поддерживать TTL для лимитных и idempotency ключей;
|
||
- **не** хранить OTP counters для `api-backend` (OTP — зона Keycloak/SPI);
|
||
- 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
|
||
|
||
Принимает telemetry от сервисов.
|
||
|
||
Требования:
|
||
|
||
- OTLP HTTP/gRPC receiver;
|
||
- экспорт traces/logs в stdout или платформенный collector;
|
||
- единые resource attributes: `service.name`, `deployment.environment`.
|
||
|
||
## Networks
|
||
|
||
Рекомендуемые сети:
|
||
|
||
- ВМ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, OTLP receivers и internal service ports не публикуются. Cross-host calls идут через private network, точные SG и TLS.
|
||
|
||
## Volumes
|
||
|
||
Минимальные volumes:
|
||
|
||
- ВМ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.
|
||
|
||
## Переменные окружения
|
||
|
||
Каждая VM имеет свой allow-listed non-secret env manifest. Секреты доставляются отдельными service files согласно arch-04/06; общий env/secret bundle двух VM запрещён.
|
||
|
||
Для 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
|
||
|
||
Соответствует [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности» (HTTPS, TLS, HSTS). Инфраструктурная реализация:
|
||
|
||
### Домены и HTTP
|
||
|
||
| Host | Порт 80 | Порт 443 | Примечание |
|
||
|---|---|---|---|
|
||
| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host |
|
||
| API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` |
|
||
| Bitrix callbacks (`/bitrix/*`, `/bitrix/sync/*`) | не обслуживает API; только redirect на том же host | HTTPS | webhook и install URL |
|
||
|
||
Правила:
|
||
|
||
- все внешние пользовательские соединения — **HTTPS**;
|
||
- HTTP допускается **только** на веб-домене как вход для редиректа на HTTPS;
|
||
- для **выделенного API-домена** HTTP **не допускается** (нет listener на :80);
|
||
- при едином домене MVP redirect на :80 применяется ко всему host, включая `/api/*` и `/auth/*`, после редиректа — только HTTPS.
|
||
|
||
### 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 и совпадение ключа;
|
||
- закрыть прямой доступ к внутренним портам контейнеров извне.
|
||
|
||
## Nginx routing для Bitrix24 Local App
|
||
|
||
`nginx` должен поддерживать отдельные server/location rules для `bitrix-local-app`.
|
||
|
||
Рекомендуемая схема:
|
||
|
||
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect 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`;
|
||
- домен или path `/bitrix/*` → `bitrix-local-app`; `/bitrix/sync/*` → `bitrix-sync`;
|
||
- `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24;
|
||
- `/bitrix/placement` доступен публично как заглушка UI настроек коннектора;
|
||
- `/health/live` и `/health/ready` для `bitrix-local-app` доступны только там, где это нужно для healthcheck и проверки Bitrix form URL;
|
||
- `/internal/openlines/v1/*` не публикуется наружу или защищается allowlist/private network плюс `Authorization: Bearer {BITRIX_INTERNAL_API_TOKEN}`;
|
||
- для `/bitrix/*` callbacks кэширование отключено;
|
||
- для `/bitrix/*` callbacks включены отдельные rate limits, но они не должны блокировать легитимные webhook-повторы Bitrix24.
|
||
|
||
## Nginx routing для bitrix-sync (CRM webhooks)
|
||
|
||
Публичный nginx ВМ2 маршрутизирует только два exact webhook CRM sync в локальный `bitrix-sync`:
|
||
|
||
- `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
|
||
|
||
Rate limits должны быть распределены по двум слоям.
|
||
|
||
`nginx`:
|
||
|
||
- ограничивает частоту запросов до попадания в API;
|
||
- держит отдельные зоны лимитов для `/auth`, `/api`, public endpoints, fallback polling, download endpoints, чтения/действий/загрузок Notification Center;
|
||
- ограничивает `client_max_body_size`;
|
||
- ограничивает загрузку файлов лимитом 5 МБ; `client_max_body_size` должен быть чуть выше бизнес-лимита для учета overhead запроса;
|
||
- применяет `limit_req` для endpoint авторизации и fallback polling;
|
||
- для публичных endpoint использует лимит не выше 60 запросов в минуту с одного IP, если настройки не говорят иначе;
|
||
- возвращает `429` при превышении лимитов;
|
||
- не должен использоваться для сложных пользовательских правил, завязанных на `user_id`.
|
||
|
||
API:
|
||
|
||
- применяет лимиты после проверки JWT;
|
||
- считает лимиты по `user_id`, IP, route, dialog id и service client;
|
||
- хранит быстрые счетчики в Redis;
|
||
- пишет значимые превышения в audit/App DB;
|
||
- возвращает `Retry-After`, если клиент может повторить запрос позже.
|
||
|
||
Проверка сообщений на prompt injection и вредоносные действия не должна выполняться в `nginx`: это задача отдельного сервиса `message-safety`, вызываемого из `api-backend` (см. [`arch-02-api-contracts.md`](arch-02-api-contracts.md)).
|
||
|
||
## WAF
|
||
|
||
WAF можно подключить внешним слоем перед `nginx` без изменения бизнес-кода, если соблюдены требования:
|
||
|
||
- `nginx` и API корректно работают с цепочкой proxy headers и доверяют real IP только от доверенных прокси;
|
||
- CORS разрешает только доверенные домены;
|
||
- публичные endpoint имеют rate limits и кэширование даже без WAF;
|
||
- схема TLS termination согласована с тем, где завершается TLS: WAF/CDN, load balancer или `nginx`;
|
||
- WAF не должен подменять тело запросов и ответы API без явной необходимости.
|
||
|
||
WAF не заменяет обязательные лимиты, валидацию схем, авторизацию и аудит внутри приложения.
|
||
|
||
## Публичные endpoint
|
||
|
||
`GET /api/v1/public/app-config` и `GET /api/v1/public/content` являются публичными, поэтому для них обязательны:
|
||
|
||
- `limit_req` на уровне `nginx`, базово 60 запросов в минуту с одного IP;
|
||
- агрессивное кэширование на уровне `nginx` или CDN;
|
||
- заголовок `Cache-Control: public, max-age=3600`;
|
||
- строгая DTO-схема ответа на backend, без сериализации всех строк таблицы настроек;
|
||
- CORS только для доверенных доменов приложения;
|
||
- отсутствие секретов, внутренних URL, service tokens и приватных feature flags в ответе.
|
||
|
||
Подробнее — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), разделы «Публичный config endpoint» и «Публичный content endpoint».
|
||
|
||
## Healthchecks
|
||
|
||
Минимальные проверки:
|
||
|
||
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
|
||
- `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;
|
||
- `redis`: `redis-cli ping`;
|
||
|
||
Наружу через `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.
|
||
|
||
## Порядок запуска
|
||
|
||
Общие 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` может стать ready для read API до ВМ2, но send endpoint обязан fail-closed при недоступной требуемой capability Safety.
|
||
|
||
## Развёртывание на ВМ1 и ВМ2
|
||
|
||
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` запрещён.
|
||
|
||
Перед первым `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-замечания
|
||
|
||
### Frontend (Expo web)
|
||
|
||
| Режим | Поведение nginx |
|
||
|---|---|
|
||
| production-like / production | Статика Expo web (`expo export` / EAS web build), `FRONTEND_DEV_PROXY_ENABLED=false` |
|
||
| local dev | Опционально proxy на Expo dev server, `FRONTEND_DEV_PROXY_ENABLED=true` |
|
||
|
||
Переменные — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), блок «Frontend (nginx)».
|
||
|
||
Два root Compose projects — production-контур первого этапа. Позже при росте можно отдельно решить:
|
||
|
||
- вынос Redis в managed cache;
|
||
- перенос `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
|
||
|
||
- Managed PostgreSQL должен иметь ежедневные backups и PITR; целевые RPO/RTO для MVP фиксируются в ops runbook до production-запуска.
|
||
- S3-data (`attachments`, `documents`) хранит production-файлы; удаление выполняется только через lifecycle, retention или явный audit-backed процесс.
|
||
- S3-quarantine очищается периодическим cleanup job: удаляются просроченные объекты без активного `MessageAttachment`/`safety_tasks` или объекты с завершённым deny/failed lifecycle.
|
||
- Redis не является единственным хранилищем бизнес-событий; потеря Redis не должна терять сообщения, sync tasks или audit.
|