245 lines
17 KiB
Markdown
245 lines
17 KiB
Markdown
# 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).
|