Files
han-app/modules/module-03-nginx.md
T

19 KiB
Raw Blame History

module-03. Проектная спецификация корневого nginx

Статус: целевая спецификация полностью рабочего edge-контура MVP.
Источники: README.md, arch-00-glossary.md, arch-01-system-architecture.md, arch-02-api-contracts.md, arch-03-docker-compose-blueprint.md, arch-04-settings-and-content.md, arch-05-agent-development-process.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:

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 получает:

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 конфигурации

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:

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.