Files
han-app/architectory/arch-08-nginx.md
T

245 lines
17 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.
# arch-08. Контракт корневого nginx
> Канонический контракт nginx для всех application VM.
> Compose-топология, сети и published ports — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
> Реализация на конкретной VM — [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md) и [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md).
> Telemetry access log — [`arch-07-observability.md`](arch-07-observability.md). Env — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Host security — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
## Назначение
Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2**: независимый nginx на каждой машине, TLS/ACME, request id, forwarded headers, запрет internal paths, JSON access log, hardening Compose и failure/reload policy.
Routing matrix, upstreams, SPA, WebSocket, CSP и allow-list конкретной машины здесь не детализируются и **не кастомизируются** так, чтобы сломать этот контракт.
## 1. Обязательная топология
В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle.
- публичный трафик одной VM не проходит через nginx другой;
- отказ или deploy одной VM не обязан прерывать ingress другой;
- контейнеры приложений не публикуют host ports; на каждой VM наружу смотрит только её nginx;
- если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно (N1).
Между public route ВМ1 и ВМ2 нет reverse-proxy chain и нет SPA/API fallback на другую VM.
## 2. Общие правила routing
Порядок location критичен. Exact locations объявляются до prefix.
На **обоих** public hosts:
- `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`;
- fallback на другую VM или чужой SPA запрещён;
- query не участвует в exact location matching;
- адрес из недоверенного `X-Forwarded-For` не используется для allow-list;
- при внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP.
Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API-подобные locations возвращают `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим только там, где профильная спецификация это разрешает, и не имитирует backend domain code.
Проверка prompt injection / malware не выполняется в nginx: это `message-safety` через `api-backend` (arch-02).
## 3. HTTP/HTTPS и TLS
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`;
- единственное route-specific исключение: exact CRM webhook ВМ2 по HTTP возвращает `404/426` без redirect, чтобы query token не отражался в `Location`;
- выделенный 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;
- `Server` tokens скрыты; upstream `X-Powered-By` удаляется.
ВМ2 дополнительно слушает private `8443` с сертификатом internal CA — детали в спецификации ВМ2.
## 4. ACME lifecycle
Выбран webroot Certbot/ACME client с host-каталогами:
```text
/etc/letsencrypt root-only ACME state; rw только у root/ACME client
/var/lib/han-chat/public-tls staged fullchain.pem/privkey.pem; ro bind в nginx
/var/lib/han-chat/acme webroot; bind в nginx и ACME client
```
Named volume для public certificate/key запрещён. Non-root nginx никогда не
монтирует `/etc/letsencrypt`: после успешного issuance/renewal root hook
проверяет certificate/key и атомарно копирует только нужные PEM в
`/var/lib/han-chat/public-tls` с `root:<dedicated-tls-group>`, directory `0750`
и files `0640`. Каждая VM выпускает **свой** сертификат на свой public host;
host-каталоги двух VM не общие.
Bootstrap:
1. DNS указывает на VM; 80/443 разрешены.
2. Подготовить root-owned webroot `/var/lib/han-chat/acme` и временный HTTP config с `/.well-known/acme-challenge/`.
3. Выпустить certificate в root-only `/etc/letsencrypt` без остановки nginx.
4. Проверить certificate/key, атомарно обновить `/var/lib/han-chat/public-tls`, выполнить `nginx -t`, активировать TLS config и reload.
Root-owned host timer выполняет `certbot renew` минимум дважды в сутки; ACME
client может быть контейнеризован, но только он получает rw bind
`/etc/letsencrypt`, а public certificate остаётся host bind, не named volume.
После фактического renewal hook проверяет пару certificate/key, атомарно
обновляет staging и проверяет рабочую конфигурацию командой
`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.
Успешный deploy/renew hook возвращает `0` с пустым stderr: вывод успешного
`nginx -t` и progress signal command перехватывается или подавляется; при
ошибке сохранённая диагностика полностью печатается в stderr. Любой stderr на
success path считается дефектом интеграции с Certbot.
Контролируются expiry days и последняя успешная попытка. Staging CA используется
в rehearsal, чтобы не исчерпать лимиты.
Non-root nginx получает только read-only staging
`/var/lib/han-chat/public-tls`; root-only `/etc/letsencrypt` ему недоступен.
## 5. 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.
Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy.
## 6. Timeouts и body limits — общие ориентиры
| Группа | connect/send/read | Владелец |
|---|---|---|
| обычный API | 3s / 30s / 30s | ВМ1 |
| auth | 3s / 30s / 60s | ВМ1 |
| Bitrix local callback | 3s / 30s / 60s | ВМ1 |
| Direct SMS callback | 3s / 30s / 60s | ВМ1 |
| WS | 3s / 30s / 75s+ | ВМ1 |
| message POST | 3s / 30s / `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s` минимум | ВМ1 |
| CRM webhook | 3s / 30s / 60s | ВМ2 |
| private Safety | по контракту Safety, не короче caller budget | ВМ2 |
Значение timeout генерируется из env template до startup; nginx не выполняет арифметику env runtime.
`client_max_body_size` global 8m по arch-04, но JSON locations получают более строгие limits, где возможно. Байты вложения не проходят через nginx/API: клиент PUT напрямую в S3. Buffering request допустим для малого JSON; для callback устанавливается bounded temp storage. Header count/size ограничены.
## 7. Edge rate limits — общие правила
`limit_req_zone` использует binary remote address. Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis.
Карта зон принадлежит спецификации VM. Новые зоны добавляются только там, затем при необходимости сюда как реестр имён.
## 8. Health
- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck;
- внешний health публикуется только если нужен мониторингу, с allow-list (N3);
- nginx health не утверждает готовность upstream;
- Docker healthcheck использует только binary, гарантированно присутствующий
и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если
их наличие не подтверждено image inventory;
- upstream `/health/ready` не агрегируется публично без решения ops.
## 9. Логи и 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 допустим при закреплённой версии (arch-07 O-TBD5). Если edge создаёт span, request id остаётся отдельным correlation key. Логи идут stdout/stderr; Docker/collector отвечает за доставку и rotation.
Parser и `service.name=nginx` — arch-07 §8.4.
## 10. Layout конфигурации
Общий каркас репозитория nginx:
```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
njs/request_id.js
scripts/{render,validate,reload-after-renew}.sh
tests/
```
Профильная спецификация добавляет только нужные snippets (`websocket.conf`, private server block, webhook allow-list). Image и modules pin по digest/version. Render использует allow-list env и fail-fast для пустых host/cert/upstream/timeouts. Секреты в rendered config не требуются.
## 11. Docker Compose
`nginx` подключён к `public` и `backend` (или эквивалентным сетям своей VM). Публикация host ports разрешена только nginx. Filesystem read-only, tmpfs для cache/run/temp/`/etc/nginx/conf.d` по arch-03, non-root где позволяет bind ports/capabilities. Host staging public TLS и ACME webroot монтируются с минимальными правами; named volume сертификатов запрещён. ACME client имеет только необходимые bind mounts/network.
Non-root nginx получает writable tmpfs только для `/etc/nginx/conf.d`, `/var/cache/nginx`, `/var/run` и `/tmp`; tmpfs задаёт явные UID/GID/mode. Основной `nginx.conf` подключает конкретный rendered include, не неограниченный wildcard, который позволил бы обойти `nginx -t`.
`depends_on` health не заменяет retry/readiness. После healthy upstream
обязателен config test с production service DNS names и reload/recreate nginx.
После recreate upstream повторяется reload policy либо используется явно
протестированный dynamic resolver. Nginx может временно отдавать bounded 502,
но не считается ready до этой post-ready проверки.
Published Docker ports сопоставляются original host destination через conntrack/`DOCKER-USER` по arch-06.
## 12. Failure behavior
- upstream down: bounded 502/504, без SPA fallback и без переноса на другую VM;
- 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.
## 13. Общий Definition of Done
- на каждой VM ровно один nginx; независимые public `80/443` и собственные сертификаты;
- TLS/ACME bootstrap, renewal и safe reload испытаны;
- internal endpoints/ports извне недоступны;
- request id и trusted forwarded headers корректны;
- JSON logs коррелируют request/trace и не содержат секретов;
- health/synthetic checks и failure tests проходят;
- image non-root/read-only насколько возможно, versions pinned;
- runbooks для cert, reload, upstream outage и rollback готовы.
Профильный DoD VM дополняет routing matrix, allow-list и listener этой машины.
## 14. Решения, допущения и TBD
**Решения:** независимые public nginx ВМ1/ВМ2; njs/module для UUID; public internal paths → 404; webroot ACME; CRM webhook приходит прямо на ВМ2; private `8443` ВМ2 только server-to-server.
**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно.
**TBD:**
- N1: доверенные WAF CIDR — по VM.
- N2: production cipher suite/OCSP.
- N3: нужен ли публичный health.
- N6: финальные burst/connection limits — по зонам VM.
- N7: certbot vs другой ACME client после ops review.
N4 (CSP Expo) и N5 (Bitrix frame ancestors) — спецификация ВМ1.
## 15. Ссылки
- Compose/сети: [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
- ВМ1: [`module-03-nginx-vm1.md`](../VM1_app/documentation/module-03-nginx-vm1.md).
- ВМ2: [`module-03-nginx-vm2.md`](../VM2_services/documentation/module-03-nginx-vm2.md).