287 lines
19 KiB
Markdown
287 lines
19 KiB
Markdown
# 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 |
|
||
| 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:
|
||
|
||
```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 |
|
||
| 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 конфигурации
|
||
|
||
```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.
|
||
- 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.
|