Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений

This commit is contained in:
mi
2026-08-19 18:24:00 +03:00
parent bbef7a30c9
commit c7a80e7256
103 changed files with 3457 additions and 3725 deletions
@@ -91,7 +91,6 @@ networks:
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 не общие.
@@ -103,6 +102,7 @@ Root Compose ВМ2 включает собственный nginx с public/priva
- Публикация портов наружу (`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, но в составе корневого контура они переопределяются общими.
- При сборке образов нужно добавлять защиту на CRLF → LF
### Обязательный container hardening
@@ -176,7 +176,7 @@ docker compose exec api-backend ruff format .
Требования:
- публикует наружу только `80` и `443` (см. политику HTTP ниже);
- публикует в internet только `80` и `443` (см. политику HTTP ниже); private `8443` ВМ2 публикуется только в VPC/SG для ВМ1 и ops;
- принимает внешний HTTPS-трафик;
- выполняет TLS termination на reverse proxy; внутренний HTTP между контейнерами — только в закрытой Docker-сети `backend`;
- non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`,
@@ -196,13 +196,7 @@ docker compose exec api-backend ruff format .
- при recreate/смене IP upstream действует та же post-ready reload policy либо
используется явно протестированный dynamic resolver; stale IP/DNS в
загруженной nginx config недопустим;
- **политика 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 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;
- применяет каноническую HTTP/HTTPS policy из [`arch-08-nginx.md`](arch-08-nginx.md): web/единый MVP host использует только `308` на HTTPS, выделенный API host не слушает `:80`; единственное route-specific исключение — HTTP exact CRM webhook ВМ2 возвращает `404/426` без redirect и без отражения query token;
- маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
@@ -415,16 +409,19 @@ Identity provider. **Обязателен** в compose-контуре с пер
## Volumes
Минимальные volumes:
Минимальные persistent volumes:
- ВМ1: Redis DB0/DB1 data, public TLS/ACME, local OTEL queue;
- ВМ1: Redis DB0/DB1 data, 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.
Public TLS и ACME на обеих VM — не named volumes. Root-only ACME state остаётся
на host в `/etc/letsencrypt`; root hook атомарно копирует только нужные
`fullchain.pem`/`privkey.pem` в `/var/lib/han-chat/public-tls`. Compose
монтирует этот staging read-only в nginx, а host webroot
`/var/lib/han-chat/acme` — в nginx и ACME client с минимально необходимыми
правами. Internal TLS certificate/key передаются отдельными Compose secrets и
не объединяются с public TLS.
Данные PostgreSQL **не** хранятся в Docker volumes — только managed PostgreSQL вне compose.
@@ -454,9 +451,9 @@ Local OTEL queue на каждой VM использует отдельный pe
| Host | Порт 80 | Порт 443 | Примечание |
|---|---|---|---|
| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host |
| Веб-домен (frontend) | только `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 | HTTPS | install/handler/placement |
| Bitrix Local App ВМ1 (`/bitrix/handler|install|placement`) | только `308` на HTTPS | HTTPS | install/handler/placement |
| CRM webhook ВМ2 (exact `/bitrix/sync/webhook/contact|alert`) | generic `404/426`, без redirect query token | HTTPS | отдельный processing host |
Правила:
@@ -468,26 +465,12 @@ Local OTEL queue на каждой VM использует отдельный pe
### 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 и совпадение ключа;
- закрыть прямой доступ к внутренним портам контейнеров извне.
TLS versions, trusted CA, HSTS/security headers, certificate validation и safe
reload принадлежат [`arch-08-nginx.md`](arch-08-nginx.md); cookie/OIDC и
application security — [`arch-01-system-architecture.md`](arch-01-system-architecture.md).
Compose применяет их через host binds из раздела «Volumes», не монтирует
root-only `/etc/letsencrypt` в nginx, не объявляет named volume public
certificates и не публикует внутренние порты.
## Nginx routing для Bitrix24 Local App
@@ -495,7 +478,7 @@ Local OTEL queue на каждой VM использует отдельный pe
Рекомендуемая схема:
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80`redirect HTTPS; `:443` — TLS + маршрутизация;
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80``308` 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 не маршрутизируется;
@@ -576,7 +559,7 @@ WAF не заменяет обязательные лимиты, валидац
Минимальные проверки:
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` HTTP 200/301 и успешная TLS handshake;
- `nginx`: на веб-домене — `308` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — успешная TLS handshake и ожидаемый route response;
- `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`;
@@ -635,7 +618,7 @@ endpoint обязан fail-closed при недоступной требуемо
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.
4. Public nginx ВМ2 публикует `80/443`; `80` обслуживает ACME, а HTTP exact CRM webhook возвращает `404/426` без 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