# arch-08. Контракт корневого nginx > Канонический контракт nginx для всех application VM. > Compose-топология, сети и published ports — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). > Реализация на конкретной VM — [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md) и [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md). > Telemetry access log — [`arch-07-observability.md`](arch-07-observability.md). Env — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Host security — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md). ## Назначение Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2**: независимый nginx на каждой машине, TLS/ACME, request id, forwarded headers, запрет internal paths, JSON access log, hardening Compose и failure/reload policy. Routing matrix, upstreams, SPA, WebSocket, CSP и allow-list конкретной машины здесь не детализируются и **не кастомизируются** так, чтобы сломать этот контракт. ## 1. Обязательная топология В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle. - публичный трафик одной VM не проходит через nginx другой; - отказ или deploy одной VM не обязан прерывать ingress другой; - контейнеры приложений не публикуют host ports; на каждой VM наружу смотрит только её nginx; - если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно (N1). Между public route ВМ1 и ВМ2 нет reverse-proxy chain и нет SPA/API fallback на другую VM. ## 2. Общие правила routing Порядок location критичен. Exact locations объявляются до prefix. На **обоих** public hosts: - `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; - fallback на другую VM или чужой SPA запрещён; - query не участвует в exact location matching; - адрес из недоверенного `X-Forwarded-For` не используется для allow-list; - при внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP. Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API-подобные locations возвращают `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим только там, где профильная спецификация это разрешает, и не имитирует backend domain code. Проверка prompt injection / malware не выполняется в nginx: это `message-safety` через `api-backend` (arch-02). ## 3. HTTP/HTTPS и TLS - public host каждой VM на `: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; - `Server` tokens скрыты; upstream `X-Powered-By` удаляется. ВМ2 дополнительно слушает private `8443` с сертификатом internal CA — детали в спецификации ВМ2. ## 4. ACME lifecycle Выбран webroot Certbot/ACME client с общими named volumes: ```text nginx-certs -> /etc/letsencrypt (rw у certbot, ro у nginx) nginx-acme-webroot -> /var/www/certbot ``` Каждая VM выпускает **свой** сертификат на свой public host. Секреты и volumes двух projects не общие. 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, чтобы не исчерпать лимиты. Non-root nginx не монтирует root-only дерево Let's Encrypt целиком: только необходимые cert/key files по arch-03/arch-06. ## 5. 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. Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy. ## 6. Timeouts и body limits — общие ориентиры | Группа | connect/send/read | Владелец | |---|---|---| | обычный API | 3s / 30s / 30s | ВМ1 | | auth | 3s / 30s / 60s | ВМ1 | | Bitrix local callback | 3s / 30s / 60s | ВМ1 | | Direct SMS callback | 3s / 30s / 60s | ВМ1 | | WS | 3s / 30s / 75s+ | ВМ1 | | message POST | 3s / 30s / `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s` минимум | ВМ1 | | CRM webhook | 3s / 30s / 60s | ВМ2 | | private Safety | по контракту Safety, не короче caller budget | ВМ2 | Значение timeout генерируется из env template до startup; nginx не выполняет арифметику env runtime. `client_max_body_size` global 8m по arch-04, но JSON locations получают более строгие limits, где возможно. Байты вложения не проходят через nginx/API: клиент PUT напрямую в S3. Buffering request допустим для малого JSON; для callback устанавливается bounded temp storage. Header count/size ограничены. ## 7. Edge rate limits — общие правила `limit_req_zone` использует binary remote address. Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. Карта зон принадлежит спецификации VM. Новые зоны добавляются только там, затем при необходимости сюда как реестр имён. ## 8. Health - внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck; - внешний health публикуется только если нужен мониторингу, с allow-list (N3); - nginx health не утверждает готовность upstream; - Docker healthcheck использует только binary, гарантированно присутствующий и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если их наличие не подтверждено image inventory; - upstream `/health/ready` не агрегируется публично без решения ops. ## 9. Логи и 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 допустим при закреплённой версии (arch-07 O-TBD5). Если edge создаёт span, request id остаётся отдельным correlation key. Логи идут stdout/stderr; Docker/collector отвечает за доставку и rotation. Parser и `service.name=nginx` — arch-07 §8.4. ## 10. Layout конфигурации Общий каркас репозитория nginx: ```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 njs/request_id.js scripts/{render,validate,reload-after-renew}.sh tests/ ``` Профильная спецификация добавляет только нужные snippets (`websocket.conf`, private server block, webhook allow-list). Image и modules pin по digest/version. Render использует allow-list env и fail-fast для пустых host/cert/upstream/timeouts. Секреты в rendered config не требуются. ## 11. Docker Compose `nginx` подключён к `public` и `backend` (или эквивалентным сетям своей VM). Публикация host ports разрешена только nginx. Filesystem read-only, tmpfs для cache/run/temp/`/etc/nginx/conf.d` по arch-03, non-root где позволяет bind ports/capabilities. Cert volumes read-only для nginx. ACME client имеет только необходимые volumes/network. Non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`, `/var/cache/nginx`, `/var/run` и `/tmp`; tmpfs задаёт явные UID/GID/mode. Основной `nginx.conf` подключает конкретный rendered include, не неограниченный wildcard, который позволил бы обойти `nginx -t`. `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 проверки. Published Docker ports сопоставляются original host destination через conntrack/`DOCKER-USER` по arch-06. ## 12. Failure behavior - upstream down: bounded 502/504, без SPA fallback и без переноса на другую VM; - 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. ## 13. Общий Definition of Done - на каждой VM ровно один nginx; независимые public `80/443` и собственные сертификаты; - TLS/ACME bootstrap, renewal и safe reload испытаны; - internal endpoints/ports извне недоступны; - request id и trusted forwarded headers корректны; - JSON logs коррелируют request/trace и не содержат секретов; - health/synthetic checks и failure tests проходят; - image non-root/read-only насколько возможно, versions pinned; - runbooks для cert, reload, upstream outage и rollback готовы. Профильный DoD VM дополняет routing matrix, allow-list и listener этой машины. ## 14. Решения, допущения и TBD **Решения:** независимые public nginx ВМ1/ВМ2; njs/module для UUID; public internal paths → 404; webroot ACME; CRM webhook приходит прямо на ВМ2; private `8443` ВМ2 только server-to-server. **Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно. **TBD:** - N1: доверенные WAF CIDR — по VM. - N2: production cipher suite/OCSP. - N3: нужен ли публичный health. - N6: финальные burst/connection limits — по зонам VM. - N7: certbot vs другой ACME client после ops review. N4 (CSP Expo) и N5 (Bitrix frame ancestors) — спецификация ВМ1. ## 15. Ссылки - Compose/сети: [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). - ВМ1: [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md). - ВМ2: [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md).