20 KiB
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 |
Notification paths внутри /api/ имеют отдельные edge-зоны: public catalog/campaigns, JWT read, actions, uploads и downloads. /internal/, /_internal/, Redis/OTLP/admin/status/config files запрещены exact prefix response 404 (допустим 403, но единообразно выбран 404). Никакого fallback internal path в SPA или общий proxy. message-safety, /internal/sms/* и /internal/notifications/* не имеют публичного 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, затем по решению opsincludeSubDomains; 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:
- DNS указывает на VM; 80/443 разрешены.
- Запустить временный HTTP config с
/.well-known/acme-challenge/. - Выпустить certificate без остановки nginx.
- Проверить
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;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 и всё равно проверяют 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'; - frame-src
'none': инструкцияinstall_appвсегда открывается в новой вкладке, iframe/модалка не поддерживается; - script-src без
unsafe-evalproduction; 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 отсутствует в логах. /internal/notifications/*снаружи всегда404; notification read/action/upload/public routes используют свои зоны и возвращают429.- CSP содержит
frame-src 'none'; инструкция проверяется как новая вкладка без embedded content.
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.