# module-03. Проектная спецификация корневого `nginx` > Статус: целевая спецификация полностью рабочего edge-контура MVP. > Источники: [`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. Если перед VM есть внешний WAF/LB, доверенные proxy CIDR задаются явно; nginx не доверяет произвольному `X-Forwarded-For`. Другой nginx на host не должен маршрутизировать сервисы по отдельности. ## 2. Routing matrix Порядок location критичен: exact/longest public routes до общего `/bitrix/`. | Внешний путь | 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 | `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files запрещены exact prefix response `404` (допустим `403`, но единообразно выбран `404`). Никакого fallback internal path в SPA или общий proxy. `message-safety` и `/internal/sms/*` не имеют публичного route. ## 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 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`; - выделенный 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 — `nginx -s reload`. Reload допускается только после `nginx -t`; при ошибке остаётся старый worker/config/cert и срабатывает alert. Контролируются 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; - `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 и всё равно проверяют application 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'`; - 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; - внешняя 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 без sensitive query, 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: nginx может стартовать при временно недоступном upstream и отдавать 502, затем восстановиться без reload. 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/v1/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. - allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах. ## 19. Definition of Done - единственный root nginx публикует только 80/443; - TLS/ACME bootstrap, renewal и safe reload испытаны; - routing matrix и route precedence покрыты; - 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 **Решения:** один nginx; njs/module для UUID; internal → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints. **Допущения:** MVP использует единый host `tohin.ru`; 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.