# 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$//' \ && /bin/sh -n ) и использовать проверку синтаксиса 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. - Архитектурная верхняя граница допустимого возраста signatures — `720` часов (30 дней); активный seed-порог `max_signature_age_hours` — `240` часов (10 дней), то есть строже предельного значения. Смена 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:` (`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.