Внесены правки в документацию
This commit is contained in:
@@ -40,6 +40,10 @@ Notification paths внутри `/api/` ВМ1 имеют отдельные edge
|
||||
|
||||
Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/<type>?token=...&ID=...` попадает в соответствующий exact route. До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. При внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP.
|
||||
|
||||
`BITRIX_SYNC_ENABLED`, public route, readiness и allow-list согласуются одним
|
||||
preflight: disabled требует `deny all;`, enabled — reviewed non-empty CIDR и
|
||||
ready receiver. Обратные комбинации блокируют deployment.
|
||||
|
||||
Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации.
|
||||
|
||||
## 3. Upstreams
|
||||
@@ -51,6 +55,11 @@ Nginx ВМ2 имеет независимые server blocks:
|
||||
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
|
||||
- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops.
|
||||
|
||||
Private `8443` также fail-closed: до утверждённого Safety cutover active
|
||||
caller allow-list содержит только `deny all;`; после cutover он совпадает с
|
||||
SG/host-firewall источниками ВМ1. Расхождение любого из трёх слоёв блокирует
|
||||
rollout.
|
||||
|
||||
| Path | Local upstream | Caller |
|
||||
|---|---|---|
|
||||
| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 |
|
||||
@@ -93,6 +102,10 @@ Renew container/host timer выполняет `certbot renew` минимум д
|
||||
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, чтобы не исчерпать лимиты.
|
||||
|
||||
@@ -219,6 +232,9 @@ Bitrix placement может требовать embedding: для exact `/bitrix/
|
||||
- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck;
|
||||
- внешний health публикуется только если нужен мониторингу, с allow-list;
|
||||
- nginx health не утверждает готовность upstream;
|
||||
- Docker healthcheck использует только binary, гарантированно присутствующий
|
||||
и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если
|
||||
их наличие не подтверждено image inventory;
|
||||
- внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и callback route;
|
||||
- upstream `/health/ready` не агрегируется публично без решения ops.
|
||||
|
||||
@@ -259,7 +275,12 @@ Image и modules pin по digest/version. Render использует allow-list
|
||||
|
||||
`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.
|
||||
`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 проверки. Resource/FD limits
|
||||
учитывают WS.
|
||||
|
||||
## 17. Failure behavior
|
||||
|
||||
|
||||
@@ -836,6 +836,14 @@ file_policy:
|
||||
|
||||
JSON Schema задаёт типы/ranges и cross-field constraints: `heartbeat_sec < lease_sec < execution_deadline_sec`, scan/pipeline timeout не больше execution deadline, TTL/retention положительны, `max_signature_age_hours` не превышает `720` часов (30 дней). `enabled_mime_types` — непустое уникальное подмножество detector manifest; `max_size_bytes` и последующие format overrides не превышают hard limits manifest. Referenced rules/detector artifacts обязаны быть доступны и пройти hash/signature verification до activation.
|
||||
|
||||
Seed/Schema/reference manifests являются immutable artifacts image. Любое их
|
||||
изменение выпускает новый `MESSAGE_SAFETY_IMAGE` digest и новую монотонную
|
||||
config version; config-admin сверяет artifact hashes с запущенным digest до
|
||||
activation. Image-only rollback после активации несовместимой schema запрещён:
|
||||
сначала создаётся новая совместимая config version (retired row повторно не
|
||||
активируется), затем выбирается совместимый image. Rollback window хранит
|
||||
только совместимые пары image digest + config schema/version.
|
||||
|
||||
### 15.2. Env и secrets Message Safety на ВМ2
|
||||
|
||||
```text
|
||||
@@ -855,7 +863,7 @@ Runtime secrets `MESSAGE_SAFETY_DATABASE_URL`, `MESSAGE_SAFETY_REDIS_URL`, `MESS
|
||||
|
||||
### 15.3. Emergency mode
|
||||
|
||||
`MESSAGE_SAFETY_MOCK_ENABLED`, `MESSAGE_SAFETY_MOCK_TEXT_FREE`, `MESSAGE_SAFETY_MOCK_FILE_FREE` читаются только из root-owned `/etc/han-chat/message-safety-mode.env`, не из repository `.env` и не из БД. Startup отклоняет placeholders, insecure production defaults, отсутствующий/невалидный active config и комбинацию `MOCK=false` при любом `*_FREE=true`.
|
||||
`MESSAGE_SAFETY_MOCK_ENABLED`, `MESSAGE_SAFETY_MOCK_TEXT_FREE`, `MESSAGE_SAFETY_MOCK_FILE_FREE` читаются только из read-only `/etc/han-chat/message-safety-mode.env` с host contract `root:han-message-safety 0640`, dedicated GID `10001`, совпадающим с primary GID контейнера; не из repository `.env` и не из БД. Bootstrap создаёт/проверяет группу до первого `up`. Отсутствие группы, mismatch GID, отрицательный read-test от container UID/GID либо доступ постороннего UID блокируют rollout. Startup отклоняет placeholders, insecure production defaults, отсутствующий/невалидный active config и комбинацию `MOCK=false` при любом `*_FREE=true`.
|
||||
|
||||
### 15.4. Performance acceptance MVP
|
||||
|
||||
|
||||
@@ -455,6 +455,10 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
- без host ports;
|
||||
- config read-only, `otel-queue` volume rw;
|
||||
- non-root, read-only rootfs, tmpfs `/tmp`, drop capabilities, no-new-privileges;
|
||||
- перед collector запускается idempotent `otel-queue-init`: one-shot без сети
|
||||
и secrets, с `user: 0:0`, `cap_drop: ALL` и только `CHOWN/FOWNER`, выставляет
|
||||
mount root `10001:10001 0700`; collector зависит от
|
||||
`service_completed_successfully`;
|
||||
- initial limit: 0.5 CPU/512 MiB, queue disk 5–10 ГБ; уточнить load test;
|
||||
- healthcheck extension;
|
||||
- restart policy с backoff;
|
||||
@@ -462,6 +466,10 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
|
||||
Collector существует отдельным экземпляром на ВМ1 и ВМ2. Каждый имеет собственный `otel-queue` volume/limits и экспортирует в private SigNoz `192.168.0.5:4317`; ВМ2 никогда не использует Docker hostname collector ВМ1. Telemetry outage/overflow fail-open для business и Safety readiness, но создаёт alert.
|
||||
|
||||
Новый named volume считается потенциально `root:root`; основной collector не
|
||||
запускается от root и volume не получает `0777`. Ownership init проверяется
|
||||
после первого create и повторного recreate.
|
||||
|
||||
Доступ к Docker socket запрещён. Для container metrics используется безопасный exporter/hostmetrics, а не unrestricted socket mount.
|
||||
|
||||
## 15. Security
|
||||
|
||||
@@ -200,6 +200,11 @@ free -h
|
||||
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
|
||||
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
|
||||
- [ ] UFW и DOCKER-USER активны после restart Docker.
|
||||
- [ ] Для published Docker ports allow rules сопоставляют original host
|
||||
destination через `conntrack --ctorigdstport`; positive/negative probes
|
||||
увеличивают counters нужных allow/deny rules после restart Docker и reboot.
|
||||
- [ ] После обновления firewall helper active `oneshot RemainAfterExit` unit
|
||||
явно перезапущен; `enable --now` не считается применением новой версии.
|
||||
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
|
||||
- [ ] NTP active; disk/swap соответствуют sizing.
|
||||
- [ ] Break-glass процедура сохранена вне VM.
|
||||
@@ -515,7 +520,10 @@ cd <BACKEND_ROOT>
|
||||
DOCKER_BUILDKIT=1 docker compose build --pull
|
||||
```
|
||||
|
||||
Build не получает production secrets. Записать image digests.
|
||||
Build не получает production secrets. Записать image digests и release file
|
||||
manifest (`path`, owner/group, mode, executable). Sync с blanket
|
||||
`--chmod=F644` запрещён: scripts, preflight и hooks устанавливаются
|
||||
`0755/0750` по manifest и проходят `test -x` до activation.
|
||||
|
||||
Frontend:
|
||||
|
||||
@@ -566,6 +574,10 @@ Volumes:
|
||||
- `otel-queue`;
|
||||
- никаких PG data volumes.
|
||||
|
||||
Каждый `otel-queue` перед collector подготавливает idempotent
|
||||
`otel-queue-init` (`10001:10001 0700`, без сети/secrets, только
|
||||
`CHOWN/FOWNER`); collector стартует только после успешного one-shot.
|
||||
|
||||
Проверить:
|
||||
|
||||
```bash
|
||||
@@ -581,7 +593,11 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
|
||||
- [ ] Только nginx публикует ports.
|
||||
- [ ] Internal services не подключены к public без причины.
|
||||
- [ ] Named volumes созданы и permissions проверены.
|
||||
- [ ] OTEL ownership init завершился `0`, а write-test проходит от collector
|
||||
UID; root collector/`0777` не используются.
|
||||
- [ ] Container resource limits/healthchecks заданы.
|
||||
- [ ] Healthcheck-команды выполнены в exact pinned digests; отсутствуют
|
||||
`ExitCode 127` и зависимости от несуществующих `wget`/`curl`.
|
||||
- [ ] `docker compose config --quiet` success.
|
||||
|
||||
## 12. Stage 9 — TLS bootstrap, фаза 1
|
||||
@@ -640,6 +656,9 @@ HSTS пока не включать. Проверить chain/hostname/redirect,
|
||||
5. записать структурированный результат и метрику времени до истечения;
|
||||
6. вернуть ненулевой exit code при ошибке, чтобы сработал alert;
|
||||
7. не удалять действующий сертификат при неуспешном renew.
|
||||
8. на success path вернуть `0` с пустым stderr; benign output `nginx -t` и
|
||||
signal command подавить/перенаправить, полную диагностику печатать только
|
||||
при ошибке.
|
||||
|
||||
Пример unit `/etc/systemd/system/han-chat-cert-renew.service`:
|
||||
|
||||
@@ -1278,6 +1297,8 @@ certbot delete active cert
|
||||
- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены;
|
||||
- `deploy` не имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units;
|
||||
- один root Compose и nginx на VM; ВМ1 и ВМ2 имеют независимые public 80/443, ВМ2 дополнительно private 8443; public ВМ2 ограничен exact CRM webhook;
|
||||
- после Safety cutover ВМ1 не содержит local Safety/Redis DB2 и вызывает ВМ2
|
||||
только по private HTTPS с проверенным CA bind;
|
||||
- container hardening и сетевые границы соответствуют arch-06;
|
||||
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
|
||||
- Redis/Collector volumes/resources/security работают;
|
||||
@@ -1294,6 +1315,38 @@ certbot delete active cert
|
||||
|
||||
Cutover gates: private TLS chain/SAN; Safety v2 PG migration и lease/fencing smoke; capability `text|links|files|worker`; S3 Gate 4; performance acceptance; egress negative tests. Safety Service Owner, Rule Pack Owner, Security Owner, Product Owner и Operations Owner фиксируют approvals. Только после них ВМ1 переключает `MESSAGE_SAFETY_URL`. Legacy v1 остаётся rollback target на ограниченное окно, но один `message_id` нельзя одновременно отправлять в v1 и v2.
|
||||
|
||||
#### Обязательный gate миграции legacy ВМ1
|
||||
|
||||
Перед переключением caller на ВМ2 существующий backend ВМ1 считается legacy и
|
||||
должен пройти отдельный migration gate:
|
||||
|
||||
1. env validator принимает только production
|
||||
`MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443`, требует
|
||||
`MESSAGE_SAFETY_CA_HOST_PATH` и проверяет hostname/SAN; cross-host Docker
|
||||
hostname и plaintext запрещены;
|
||||
2. internal CA расположен в едином root-owned staging path, а не под
|
||||
`deploy:deploy 0700`; positive read-test проходит от UID/GID
|
||||
`api-backend`, negative — от постороннего UID;
|
||||
3. local `message-safety`, его Redis DB2 и локальные rules-version env удалены
|
||||
из target Compose/validator. Одновременный local и remote Safety запрещён;
|
||||
4. release tree/Compose/unit принадлежат root, `deploy` исключён из `docker`,
|
||||
установлен реальный root-owned stack systemd unit и permission preflight;
|
||||
5. VM1 `DOCKER-USER` сопоставляет original published ports через
|
||||
`conntrack --ctorigdstport`; counters подтверждены positive/negative probe
|
||||
после Docker restart и reboot;
|
||||
6. images закреплены digest, release file modes подтверждены manifest,
|
||||
rollback выбирает совместимые digests/config, а не только строковый
|
||||
`RELEASE_VERSION`;
|
||||
7. ordered startup из этого runbook заменяет legacy `docker compose up -d`
|
||||
всего стека; после readiness выполняется production nginx config
|
||||
test/reload;
|
||||
8. `han-secrets` и firewall oneshot после обновления явно перезапущены и
|
||||
проверены по timestamp/live state; TLS renewal success path имеет пустой
|
||||
stderr.
|
||||
|
||||
Ни старый deployment guide, ни успешный запуск legacy single-VM Compose не
|
||||
являются доказательством прохождения этого gate.
|
||||
|
||||
Rollback возвращает caller adapter/upstream ВМ1 на предыдущий immutable release. Уже созданные v2 tasks завершаются/reconcile по PG checkpoints; down-migration и удаление quarantine versions запрещены.
|
||||
|
||||
При потере ВМ2 fail-open запрещён. ВМ2 reprovision-ится из immutable image/config; secrets materialize под отдельным IAM, Redis поднимается пустым, migrations/capability/egress gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год.
|
||||
|
||||
Reference in New Issue
Block a user