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

275 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| `/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` не имеет публичного route.
## 3. Upstreams
Именованные upstream: `api_backend`, `keycloak`, `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 |
| 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 для повторов;
- `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.
## 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.
## 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.