Files
han-app/architectory/arch-03-docker-compose-blueprint.md
T

58 KiB
Raw Blame History

arch-03. Docker Compose blueprint

Термины (имена бакетов S3, идентификаторы) — в arch-00-glossary.md. Контракт Message Safety Service — в arch-02-api-contracts.md, раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в arch-04-settings-and-content.md. VM/SSH, OS-роли, секреты, systemd-деплой и hardening — в arch-06-service-hosting-security.md. Детальный контракт nginx (TLS/ACME, request id, internal 404, reload) — arch-08-nginx.md; routing matrix VM — module-03-nginx-vm1.md и module-03-nginx-vm2.md.

Назначение

Этот документ описывает целевой Docker Compose контур для первой production-like среды. Он не заменяет будущий docker-compose.yml, но задает требования, которым он должен соответствовать.

Требования к безопасности на уровне приложения и данных — в arch-01-system-architecture.md, раздел «Принципы безопасности». Настоящий документ описывает только инфраструктурную реализацию этих принципов в compose/nginx: TLS, маршрутизация, сетевые границы, rate limits на edge. Значения переменных окружения — в 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/smssms-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.

После Safety cutover root Compose ВМ1 не содержит local message-safety, его Redis DB2 или bitrix-sync: это компоненты ВМ2. api-backend использует MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443 и отдельный read-only CA bind из root-owned staging-каталога, доступного фактическому UID/GID api-backend. Validator обязан принимать private HTTPS hostname/SAN и отклонять cross-host Docker DNS либо plaintext production URL. Наличие legacy local Safety одновременно с remote URL является cutover error, а не автоматическим fallback.

Структура Compose через include

Каждый сервис описывается в собственном docker-compose.yml и подключается в root-файл своей VM директивой include:

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 (принципиальная схема):

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:

  • непривилегированный 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;
  • bind file проверяется реальным read-test от container UID/GID и отказом постороннему UID, а не только host stat;
  • новый named volume для non-root consumer подготавливается idempotent one-shot init service с минимальными capabilities; consumer использует depends_on: condition: service_completed_successfully;
  • healthcheck именно того процесса, который реально запущен в контейнере, и только binaries, наличие которых доказано внутри exact pinned digest;
  • 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. root:root 0600 несовместим с non-root consumer; dedicated group/ACL должен совпадать с фактическим container GID и не давать доступ постороннему UID.

При сборке images необхоидмо добавлять нормализацию CRLF→LF (например, RUN sed -i 's/\r$//' <directory/service-name>
&& /bin/sh -n <directory/service-name>) и использовать проверку синтаксиса entrypoint

Команды разработки

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;
  • после readiness upstream выполняется повторный nginx -t с production Docker DNS names и контролируемый reload/recreate nginx. Первый ordered start и depends_on: service_started не гарантируют, что hostname уже резолвится;
  • при recreate/смене IP upstream действует та же post-ready reload policy либо используется явно протестированный dynamic resolver; stale IP/DNS в загруженной nginx config недопустим;
  • политика HTTP/HTTPS по доменам (каноническое правило — arch-01-system-architecture.md, «Принципы безопасности»):
    • веб-домен (frontend, SPA, статика): listen 80 допускается только для безусловного редиректа 301/308 на HTTPS; обработка бизнес-логики по HTTP запрещена;
    • API-домен (если выделен отдельный host, напр. api.example.ru): не слушает порт 80; только listen 443 ssl; HTTP-запросы к API-домену недоступны;
    • единый домен MVP (напр. tohin.ru с путями /api/*, /auth/*, web): считается веб-доменом; порт 80 — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS;
    • auth на том же host, что API (/auth/*): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host);
    • Bitrix local-app callbacks ВМ1 (/bitrix/handler|install|placement): только HTTPS; порт 80 — только redirect;
    • CRM sync webhook ВМ2 (exact /bitrix/sync/webhook/contact|alert): только HTTPS на отдельном processing host; HTTP не отражает query token в redirect;
  • маршрутизирует /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, раздел «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 /etc/han-chat/message-safety-mode.env с host contract root:han-message-safety 0640 и GID 10001; менять его и перезапускать 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, переменные 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_hours240 часов (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. Каноническая постановка — 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_YANDEX_CAPTCHA_ENABLED=true Keycloak подключается к egress с destination allow-list только для server-side validation Yandex SmartCaptcha, при false egress у Keycloak отсутствует;
  • healthcheck;
  • взаимодействия — arch-02-api-contracts.md, «Frontend ↔ Keycloak», и 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);
  • детальный контракт и карта ключей — arch-09-redis.md, module-04-redis-vm1.md, module-04-redis-vm2.md.

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.
  • ВМ1 egress подключается только к процессам с назначением: sms-worker → i-Digital Direct; Keycloak → SmartCaptcha только при включённом feature flag; api-backend → S3 и private PG, а вызов Safety идёт к processing.internal:8443; local collector → private SigNoz. sms-service без совмещённого worker, bitrix-local-app и Redis не получают общий internet egress.
  • ВМ2 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.

Local OTEL queue на каждой VM использует отдельный persistent named volume. Перед collector запускается otel-queue-init: ограниченный one-shot service выставляет mount root в 10001:10001 0700, не имеет сети/secrets и завершается до старта collector. Collector остаётся non-root; запуск collector от root, chmod 0777 и надежда на автоматическое наследование ownership из image запрещены.

Переменные окружения

Каждая 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, «Принципы безопасности» (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 Local App ВМ1 (`/bitrix/handler install placement`) только redirect на web host
CRM webhook ВМ2 (exact `/bitrix/sync/webhook/contact alert`) generic 404/426, без redirect query token HTTPS

Правила:

  • все внешние пользовательские соединения — 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;
  • только /bitrix/handler, /bitrix/install, /bitrix/placement на ВМ1 → bitrix-local-app; CRM /bitrix/sync/* на этом host не маршрутизируется;
  • 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;
  • BITRIX_SYNC_ENABLED=false требует активный edge allow-list только с deny all;; наличие allow при disabled и отсутствие reviewed allow при enabled являются preflight error;
  • /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).

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, разделы «Публичный 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 считается валидным только после запуска внутри exact production image digest с теми же user, read_only, mounts и dropped capabilities. Команда не может предполагать наличие curl, wget, shell или package, которого нет в image/SBOM; ExitCode 127 является дефектом release, а не unhealthy приложения. Для minimal nginx предпочтителен гарантированно доступный native binary/config test либо специально включённый и проверенный client.

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, otel-queue-init, затем 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-queue-init, затем 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 не заменяет проверку готовности. Для edge применяются три слоя: ordered start, readiness dependencies и post-ready config test/reload с production DNS names. Сервисы должны уметь ждать зависимости или корректно деградировать. 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 запрещён.

Host firewall обеих VM учитывает post-DNAT semantics DOCKER-USER: policy published nginx ports сопоставляет original host destination через conntrack, как определено в arch-06. Проверка только UFW INPUT или текущего container --dport не является достаточной.

Перед первым up и после смены любого image digest обязательны permission gates:

  1. проверить LF/shebang и root ownership release-артефактов;
  2. проверить manifest modes и executable bit scripts/preflight/hooks; blanket F644 для всего release запрещён;
  3. сверить UID/GID контейнеров с owner/mode host bind mounts, secret files и TLS staging;
  4. проверить доступ целевого UID и отказ постороннему UID, включая traverse parent directories;
  5. выполнить/проверить one-shot ownership init для non-root named volumes;
  6. выполнить PEM parse/key-match и Compose render;
  7. запустить image-native config test/entrypoint/healthcheck с production hardening и временными test-only upstream values;
  8. выдать migration-role только необходимые временные cross-schema grants, выполнить Alembic и отозвать grants владельцем;
  9. после readiness upstream повторить edge config test/reload с production Docker DNS names;
  10. после старта проверить отсутствие 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, блок «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.