Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
+48 -14
View File
@@ -1,39 +1,68 @@
# module-03. Проектная спецификация корневого `nginx`
> Статус: целевая спецификация полностью рабочего edge-контура MVP.
> Статус: целевая спецификация независимых nginx-контуров ВМ1 и ВМ2.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
## 1. Назначение и обязательная топология
В production-like контуре существует ровно один корневой nginx. Только он публикует host-порты `80/443`, завершает TLS, раздаёт SPA и проксирует публичные маршруты. Контейнеры API, Keycloak, Redis, Safety, Bitrix и OTEL используют только `expose`/Docker networks.
В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle:
Если перед VM есть внешний WAF/LB, доверенные proxy CIDR задаются явно; nginx не доверяет произвольному `X-Forwarded-For`. Другой nginx на host не должен маршрутизировать сервисы по отдельности.
- nginx ВМ1 обслуживает frontend/API/auth/Open Lines/SMS;
- nginx ВМ2 напрямую обслуживает публичные CRM webhook `bitrix-sync` и private Message Safety API;
- публичный трафик ВМ2 не проходит через ВМ1;
- отказ или deploy ВМ1 не прерывает приём CRM webhook на ВМ2.
Контейнеры приложений не публикуют host ports. На каждой VM наружу смотрит только её nginx. Если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно.
## 2. Routing matrix
Порядок location критичен: exact/longest public routes до общего `/bitrix/`.
Порядок location критичен. Публичные route разделены по host/VM.
### ВМ1
| Внешний путь | Upstream | Режим |
|---|---|---|
| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS |
| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer |
| exact `/callbacks/idgtl/sms` | `sms-service:8080` | public HTTPS POST Direct; IP allowlist + Basic auth в upstream |
| `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS POST, no cache; отсутствует, пока действует stub module-07 |
| `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` | `bitrix-local-app:8080` | public HTTPS |
| exact `/health/live`, `/health/ready` | `bitrix-local-app:8080` | по умолчанию не публикуются; только при явно выбранной ops/monitoring policy |
| `/` | static SPA либо Expo dev upstream | `try_files` fallback |
Notification paths внутри `/api/` имеют отдельные edge-зоны: public catalog/campaigns, JWT read, actions, uploads и downloads. `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files запрещены exact prefix response `404` (допустим `403`, но единообразно выбран `404`). Никакого fallback internal path в SPA или общий proxy. `message-safety`, `/internal/sms/*` и `/internal/notifications/*` не имеют публичного route.
### ВМ2
| Внешний путь | Upstream | Режим |
|---|---|---|
| exact `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS Contact event; source IP CIDR/method/body/rate limits, query-token auth в upstream |
| exact `/bitrix/sync/webhook/alert` | `bitrix-sync:8080` | public HTTPS smart-process event; те же ограничения |
Notification paths внутри `/api/` ВМ1 имеют отдельные edge-зоны. На обоих public hosts `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; fallback на другую VM или SPA запрещён. До full sync cutover оба exact webhook route ВМ2 закрыты либо возвращают retryable `503`; успешный `2xx ignored` запрещён.
Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/<type>?token=...&ID=...` попадает в соответствующий exact route. До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. При внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP.
Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации.
## 3. Upstreams
Именованные upstream: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, `bitrix_sync`, опционально `frontend_dev`. Для одной replica допустим `server service:port`; `keepalive` включён. Docker DNS resolver задаётся с коротким `valid` и `resolve` там, где поддерживает выбранная nginx edition; иначе контейнер перезапускается при смене IP upstream.
Именованные upstream ВМ1: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, опционально `frontend_dev`, а также private `processing_gateway` только для вызовов Message Safety из api-backend.
Nginx ВМ2 имеет независимые server blocks:
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops.
| Path | Local upstream | Caller |
|---|---|---|
| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 |
| `/internal/sync/v1/*` | `bitrix-sync:8080` | ops/allow-listed service |
Public и private server blocks не имеют общего fallback. Все прочие paths/methods возвращают `404/405`. Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy.
Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API возвращает `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим для `/api`, но не имитирует backend domain code.
## 4. HTTP/HTTPS и TLS
- единый web host `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`;
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`; исключение — `/bitrix/sync/webhook/contact|alert`, которые возвращают generic `404/426` без redirect и отражения query token;
- выделенный API host, если появится, не имеет listener `:80`;
- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy;
- сертификат доверенного CA, private key read-only и недоступен приложению;
@@ -134,7 +163,7 @@ traceparent: входной валидный либо новый согласн
- `ws_connect`: handshake;
- `connections`: `limit_conn`.
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst и всё равно проверяют application token в сервисе.
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst, проходят source IP allow-list и проверяют query receiver token в сервисе.
## 10. Static SPA и dev mode
@@ -195,7 +224,7 @@ Bitrix placement может требовать embedding: для exact `/bitrix/
## 14. Логи и OTEL correlation
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI без sensitive query, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI на основе `$uri` без `$request_uri`/`$args`, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
Не логируются Authorization, cookies, request/response body, OTP, tokens, query token, presigned query, PII. Error log структурирован настолько, насколько позволяет nginx; debug выключен production.
@@ -253,7 +282,7 @@ docker compose exec nginx nginx -t
curl -I http://tohin.ru/
curl -vk https://tohin.ru/api/v1/public/app-config
openssl s_client -connect tohin.ru:443 -servername tohin.ru
curl -i https://tohin.ru/internal/safety/v1/messages/check
curl -i https://tohin.ru/internal/safety/v2/messages/check
```
Автоматические тесты:
@@ -271,15 +300,20 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
- CSP/CORS preflight и Bitrix placement exception;
- upstream down/timeout, failed reload, renewal rehearsal;
- logs не содержат secrets/query tokens.
- CRM webhook exact routes принимают query без изменения location matching; allowed source IP проксируется, wrong IP получает `403` до upstream;
- HTTP webhook URL с query token не перенаправляется на HTTPS и не отражает query в `Location`/error;
- source-IP rejects попадают в безопасный bounded-retention журнал и low-cardinality telemetry без query/body/IP label;
- allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах.
- `/internal/notifications/*` снаружи всегда `404`; notification read/action/upload/public routes используют свои зоны и возвращают `429`.
- CSP содержит `frame-src 'none'`; инструкция проверяется как новая вкладка без embedded content.
## 19. Definition of Done
- единственный root nginx публикует только 80/443;
- на каждой VM ровно один nginx; ВМ1 и ВМ2 независимо публикуют только свои утверждённые `80/443`, ВМ2 дополнительно слушает private `8443`;
- TLS/ACME bootstrap, renewal и safe reload испытаны;
- routing matrix и route precedence покрыты;
- CRM webhook достигает ВМ2 напрямую и продолжает приниматься при остановленном nginx ВМ1;
- CRM webhook ограничен version-controlled source IP CIDR allow-list; query token и form body отсутствуют в access/error logs и traces;
- internal endpoints/ports извне недоступны;
- WS работает на `/api/v1/realtime`;
- message timeout равен safety max + минимум 30 секунд;
@@ -293,8 +327,8 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
## 20. Решения, допущения и TBD
**Решения:** один nginx; njs/module для UUID; internal → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
**Решения:** независимые public nginx ВМ1/ВМ2; CRM webhook приходит прямо на ВМ2 через source IP CIDR allow-list; private `8443` ВМ2 используется только server-to-server; njs/module для UUID; public internal paths → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
**Допущения:** MVP использует единый host `tohin.ru`; upstream service names стабильны в Compose; S3 CORS настраивается отдельно.
**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно.
**TBD:** N1 доверенные WAF CIDR; N2 production cipher suite/OCSP; N3 нужен ли публичный health; N4 точный CSP Expo build; N5 Bitrix frame ancestor domains; N6 финальные burst/connection limits; N7 certbot vs другой ACME client после ops review.