# module-03. Проектная спецификация корневого `nginx` > Статус: целевая спецификация независимых 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 контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle: - 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 критичен. Публичные 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/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 | ### ВМ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/?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. `BITRIX_SYNC_ENABLED`, public route, readiness и allow-list согласуются одним preflight: disabled требует `deny all;`, enabled — reviewed non-empty CIDR и ready receiver. Обратные комбинации блокируют deployment. Запрос вне 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 ВМ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. Private `8443` также fail-closed: до утверждённого Safety cutover active caller allow-list содержит только `deny all;`; после cutover он совпадает с SG/host-firewall источниками ВМ1. Расхождение любого из трёх слоёв блокирует rollout. | 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 - 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 и недоступен приложению; - OCSP stapling при поддержке CA/DNS; - HSTS включается только после успешной проверки HTTPS: `max-age` из env, затем по решению ops `includeSubDomains`; preload не включать автоматически; - OIDC redirects, cookies и external URLs всегда HTTPS. ## 5. ACME lifecycle Выбран webroot Certbot/ACME client с общими named volumes: ```text nginx-certs -> /etc/letsencrypt (rw у certbot, ro у nginx) nginx-acme-webroot -> /var/www/certbot frontend-static -> /usr/share/nginx/html:ro ``` Bootstrap: 1. DNS указывает на VM; 80/443 разрешены. 2. Запустить временный HTTP config с `/.well-known/acme-challenge/`. 3. Выпустить certificate без остановки nginx. 4. Проверить `nginx -t`, атомарно активировать TLS config, reload. Renew container/host timer выполняет `certbot renew` минимум дважды в сутки; после фактического renewal проверяет рабочую конфигурацию командой `docker compose exec -T nginx nginx -t -c /tmp/nginx.conf` и отправляет master-процессу `docker compose kill -s HUP nginx`. Bare-команды `nginx -t` и `nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся в `/tmp`. При ошибке остаётся старый worker/config/cert и срабатывает alert. Успешный deploy/renew hook возвращает `0` с пустым stderr: вывод успешного `nginx -t` и progress signal command перехватывается или подавляется; при ошибке сохранённая диагностика полностью печатается в stderr. Любой stderr на success path считается дефектом интеграции с Certbot. Контролируются expiry days и последняя успешная попытка. Staging CA используется в rehearsal, чтобы не исчерпать лимиты. ## 6. Request ID и forwarded headers На edge формируется trusted request id. Базовый nginx не генерирует UUID штатной переменной, поэтому используется njs/Lua либо модуль request-id, включённый в закреплённый image. Входящий `X-Request-ID` принимается только если соответствует UUID/ULID и длине; иначе генерируется новый. Upstream получает: ```text Host: original host X-Real-IP: trusted real client IP X-Forwarded-For: normalized proxy chain X-Forwarded-Proto: https X-Forwarded-Host: original host X-Forwarded-Port: 443 X-Request-ID: edge request id traceparent: входной валидный либо новый согласно OTEL integration ``` Ответ всегда содержит `X-Request-ID`. Клиентские `X-Forwarded-*` от недоверенного адреса перезаписываются. `Authorization`, `Cookie`, query string и body не попадают в access log. ## 7. WebSocket Только exact `location = /api/v1/realtime`: - `proxy_http_version 1.1`; - `Upgrade $http_upgrade`, `Connection` через `map`; - buffering и response cache выключены; - read timeout больше ping interval (ориентир 75 с), send timeout bounded; - rate limit handshake и `limit_conn` на IP; - query `access_token` вырезается/редактируется из логов; - subprotocol передаётся; - при shutdown nginx позволяет grace reconnect, frontend восстанавливается polling. ## 8. Timeouts и body limits Общие ориентиры: | Группа | connect/send/read | |---|---| | обычный API | 3s / 30s / 30s | | auth | 3s / 30s / 60s | | Bitrix callback | 3s / 30s / 60s | | Direct SMS callback | 3s / 30s / 60s | | WS | 3s / 30s / 75s+ | | message POST | 3s / 30s / `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s` минимум | При default safety max 300 сек message read timeout не меньше 330 сек. Значение генерируется из env template до startup; nginx не выполняет арифметику env runtime. `client_max_body_size` global 8m по arch-04, но JSON API locations получают более строгие limits, где возможно. Байты вложения не проходят через nginx/API: клиент PUT напрямую в S3. Buffering request допустим для малого JSON; для callback устанавливается bounded temp storage. Header count/size ограничены. ## 9. Edge rate limits `limit_req_zone` использует binary remote address и отдельные зоны: - `auth`: `NGINX_RATE_LIMIT_AUTH`, малый burst, без большого nodelay; - `public`: config/content, 60/min/IP; - `api`: общий API; - `polling`: GET messages fallback; - `downloads`: issuance URL; - `notifications_read`: list/counter/detail; - `notifications_action`: read/hide/CTA/button; - `notification_upload`: универсальные upload drafts; - `notifications_public`: guest notifications и каталог видов; - `bitrix_callbacks`: мягкий burst для повторов; - `idgtl_callbacks`: отдельный bounded burst, учитывающий повтор каждые 5 минут в течение суток; - `ws_connect`: handshake; - `connections`: `limit_conn`. Ответ превышения — `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 Production: - root `${FRONTEND_STATIC_PATH}`; - существующие hashed assets — `Cache-Control: public, max-age=31536000, immutable`; - `index.html`, manifest/service worker — `no-cache` либо короткая revalidation; - `try_files $uri $uri/ /index.html`; - dotfiles, source maps (если не предназначены), config/env artifacts запрещены; - API/Bitrix/auth/internal locations объявлены до SPA и никогда в неё не fallback. Dev: при `FRONTEND_DEV_PROXY_ENABLED=true` `/` проксируется на allow-listed `EXPO_DEV_SERVER_URL`, с WS/HMR. Этот режим запрещён при `APP_ENV=production-like|production`; startup template validator fail-fast. ## 11. Public caching `GET /api/v1/public/app-config` и `/content` кэшируются только для GET/HEAD, с key `scheme+host+uri+accept-encoding` (и locale query, если контракт его использует). Backend `Cache-Control`/ETag учитываются. Базовый TTL — `security.public_cache.max_age_seconds`/3600. - `Set-Cookie` не кэшируется; - Authorization request bypass cache; - stale-if-error допускается ограниченно и маркируется `Warning`; - mutation, auth, Bitrix, profile, dialogs, downloads и errors не кэшируются; - cache status пишется в log, но наружу технологический header опционален. ## 12. CORS, CSP и security headers CORS — exact allow-list из согласованного deploy config; application CORS остаётся последней инстанцией. Wildcard с credentials запрещён. Allowed headers: `Authorization`, `Content-Type`, `X-Request-ID`, `X-Ux-Session-Id`, `Idempotency-Key`, `traceparent`; методы соответствуют OpenAPI. Preflight получает bounded max-age. Для SPA: - CSP default-src `'self'`; - connect-src `'self'` `https:` к разрешённому S3 endpoint и `wss:` текущего host; - img-src `'self' data: blob:` и разрешённые signed HTTPS resources; - object-src `'none'`, base-uri `'self'`, frame-ancestors `'none'`; - frame-src `'none'`: инструкция `install_app` всегда открывается в новой вкладке, iframe/модалка не поддерживается; - script-src без `unsafe-eval` production; nonce/hash при необходимости; - style-src policy согласовать с Expo build, постепенно исключить unsafe-inline. Также: `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`, frame protection через CSP, корректный COOP/CORP без поломки Keycloak redirect/S3. `Server` tokens скрыты; upstream `X-Powered-By` удаляется. Bitrix placement может требовать embedding: для exact `/bitrix/placement` CSP `frame-ancestors` задаётся отдельным allow-list Bitrix24, а не ослабляет SPA. ### Callback i-Digital Direct - Только exact `location = /callbacks/idgtl/sms`; разрешён только `POST`, остальные методы отклоняются. - Source IP allowlist — `185.203.96.7`, но значение обязательно повторно сверяется с актуальной документацией Direct перед production. При WAF/LB используется только нормализованный trusted client IP. - TLS обязателен; cache выключен; body size ограничен под массив callback items. - Basic `Authorization` передаётся `sms-service`, но никогда не записывается в access/error logs. URL с credentials также редактируется. - Nginx не проверяет provider payload и не преобразует статусы; это делает `sms-service`. Ошибку upstream/DB нельзя маскировать `2xx`, иначе Direct не повторит callback. ## 13. Health - внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck; - внешний health публикуется только если нужен мониторингу, с allow-list; - nginx health не утверждает готовность upstream; - Docker healthcheck использует только binary, гарантированно присутствующий и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если их наличие не подтверждено image inventory; - внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и callback route; - upstream `/health/ready` не агрегируется публично без решения ops. ## 14. Логи и OTEL correlation 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. Nginx передаёт W3C trace context; native OTEL module допустим при закреплённой версии. Если edge создаёт span, request id остаётся отдельным correlation key. Логи идут stdout/stderr; Docker/collector отвечает за доставку и rotation. ## 15. Layout конфигурации ```text nginx/ Dockerfile docker-compose.yml nginx.conf templates/ 00-maps.conf.template 10-upstreams.conf.template 20-http-redirect.conf.template 30-https-site.conf.template snippets/ proxy-common.conf security-headers.conf tls.conf rate-limits.conf websocket.conf njs/request_id.js scripts/{render,validate,reload-after-renew}.sh tests/ ``` Image и modules pin по digest/version. Render использует allow-list env и fail-fast для пустых host/cert/upstream/timeouts. Секреты в rendered config не требуются. ## 16. Docker Compose `nginx` подключён к `public` и `backend`, публикует `${NGINX_HTTP_PORT}:80`, `${NGINX_HTTPS_PORT}:443`; filesystem read-only, tmpfs для cache/run/temp, non-root где позволяет bind ports/capabilities. Cert/static volumes read-only. ACME client имеет только необходимые volumes/network. `depends_on` health не заменяет retry/readiness. После healthy upstream обязателен config test с production service DNS names и reload/recreate nginx. После recreate upstream повторяется reload policy либо используется явно протестированный dynamic resolver. Nginx может временно отдавать bounded 502, но не считается ready до этой post-ready проверки. Resource/FD limits учитывают WS. ## 17. Failure behavior - API/Keycloak upstream down: bounded 502/504, без SPA fallback. - Safety slow: nginx ждёт message budget ≥ max+30, затем 504; backend checkpoint продолжает recovery. - Redis down не влияет на запуск nginx; app решает degraded policy. - cert renewal failed: текущий cert продолжает работу, alert до expiry. - invalid new config: reload отменяется, старые workers остаются. - disk/cache full: public cache bypass/evict, requests продолжаются где безопасно. - DNS upstream changed: resolver/restart policy восстанавливает адрес. - overload: 429/503 на edge, bounded queues; не накапливать неограниченные connections. ## 18. Валидация и тесты Команды acceptance: ```text docker compose config 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/v2/messages/check ``` Автоматические тесты: - Test::Nginx/containers для route precedence, methods, 404 internal; - TLS scan: только 1.2/1.3, chain/hostname/expiry; - redirect и ACME challenge; - request-id valid/invalid/generation/propagation; - forwarded spoof rejection; - WS handshake, ping idle и reconnect; - message request длительнее safety max не обрывается до budget; - body/header limits; - rate zones/429/Retry-After; - public cache HIT/MISS/bypass/no private cache; - 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 - на каждой 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 секунд; - request id и trusted forwarded headers корректны; - limits, public cache, CSP/CORS/security headers проверены; - static production и dev proxy guard работают; - JSON logs коррелируют request/trace и не содержат секретов; - health/synthetic checks и failure tests проходят; - image non-root/read-only насколько возможно, versions pinned; - runbooks для cert, reload, upstream outage и rollback готовы. ## 20. Решения, допущения и TBD **Решения:** независимые 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. **Допущения:** ВМ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.