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

24 KiB
Raw Blame History

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

Статус: целевая спецификация независимых nginx-контуров ВМ1 и ВМ2.
Источники: 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 контуре каждая 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/<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 ВМ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

  • 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:

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. Контролируются 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;
  • 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;
  • внешняя 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 конфигурации

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/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.