Внесены правки в документацию

This commit is contained in:
mi
2026-08-14 11:51:21 +03:00
parent 9eb8b2bc6e
commit e06a77ee1d
9 changed files with 286 additions and 22 deletions
+7
View File
@@ -77,5 +77,12 @@
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
- Новый env или ключ `app_settings` → arch-04.
- Новая VM, изменение сетевой доступности, прав `deploy`, sudo/systemd, capabilities, volumes или способа доставки секретов → arch-06 (+ arch-03/arch-04 и deployment runbook).
- Изменение host bind owner/mode, named-volume ownership init, healthcheck
command/image digest, published Docker port/`DOCKER-USER`, release file
modes или renewal/systemd hook → arch-06 + arch-03 + профильный module +
runbook.
- Изменение service seed/schema/embedded artifact или feature flag, влияющего
на edge route/allow-list → arch-04 + профильный module + rollout/rollback
gates в module-10.
- Новый термин / enum → arch-00, затем поиск по arch-*.
- Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в arch-*.
@@ -27,6 +27,15 @@
- **Nginx ВМ2** независимо терминирует public HTTPS на отдельном host и публикует только exact Contact/alert webhook `bitrix-sync`. Отдельный private listener `8443` по internal CA маршрутизирует allow-listed Message Safety/internal paths.
- Между public route ВМ1 и ВМ2 нет reverse-proxy chain или fallback. Каждый nginx имеет собственные DNS, сертификат, rate limits и release lifecycle.
После Safety cutover root Compose ВМ1 не содержит local `message-safety`,
его Redis DB2 или `bitrix-sync`: это компоненты ВМ2. `api-backend` использует
`MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443` и отдельный read-only CA
bind из root-owned staging-каталога, доступного фактическому UID/GID
`api-backend`. Validator обязан принимать private HTTPS hostname/SAN и
отклонять cross-host Docker DNS либо plaintext production URL. Наличие legacy
local Safety одновременно с remote URL является cutover error, а не
автоматическим fallback.
### Структура Compose через `include`
Каждый сервис описывается в собственном `docker-compose.yml` и подключается в root-файл своей VM директивой `include`:
@@ -119,7 +128,13 @@ Root Compose ВМ2 включает собственный nginx с public/priva
logs и persistent state;
- отдельный volume/tmpfs для каждого writable path с явными
`uid/gid/mode`, размером и mount flags;
- healthcheck именно того процесса, который реально запущен в контейнере;
- bind file проверяется реальным read-test от container UID/GID и отказом
постороннему UID, а не только host `stat`;
- новый named volume для non-root consumer подготавливается idempotent
one-shot init service с минимальными capabilities; consumer использует
`depends_on: condition: service_completed_successfully`;
- healthcheck именно того процесса, который реально запущен в контейнере, и
только binaries, наличие которых доказано внутри exact pinned digest;
- restart semantics: успешный one-shot exit не должен превращаться в
бесконечный restart/download loop.
@@ -131,6 +146,8 @@ mount, а не `read_only: false`, root, `privileged` или broad capabilities.
Compose file secrets с bind-backed `file:` могут игнорировать декларативные
`uid/gid/mode`. Их фактические host permissions создаёт secret materializer;
rollout проверяет owner/mode из контейнера и с host, не полагаясь на YAML.
`root:root 0600` несовместим с non-root consumer; dedicated group/ACL должен
совпадать с фактическим container GID и не давать доступ постороннему UID.
При сборке images необхоидмо добавлять нормализацию CRLF→LF
(например, RUN sed -i 's/\r$//' <directory/service-name> \
@@ -172,6 +189,13 @@ docker compose exec api-backend ruff format .
- pre-start config test выполняет реальный image entrypoint. До запуска
upstream-контейнеров их host variables временно подменяются loopback IP
только в test container; production Compose сохраняет service DNS names;
- после readiness upstream выполняется повторный `nginx -t` с production
Docker DNS names и контролируемый reload/recreate nginx. Первый ordered
start и `depends_on: service_started` не гарантируют, что hostname уже
резолвится;
- при recreate/смене IP upstream действует та же post-ready reload policy либо
используется явно протестированный dynamic resolver; stale IP/DNS в
загруженной nginx config недопустим;
- **политика HTTP/HTTPS по доменам** (каноническое правило — [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности»):
- **веб-домен** (frontend, SPA, статика): `listen 80` допускается **только** для безусловного редиректа `301`/`308` на HTTPS; обработка бизнес-логики по HTTP запрещена;
- **API-домен** (если выделен отдельный host, напр. `api.example.ru`): **не** слушает порт `80`; только `listen 443 ssl`; HTTP-запросы к API-домену недоступны;
@@ -235,7 +259,11 @@ Python FastAPI backend.
- runtime role читает immutable active `message_safety.config_versions`; создавать/активировать config может только отдельный migration/config-admin job;
- использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
- запускает async workers для file scan из S3-quarantine;
- API container получает read-only root-owned `/etc/han-chat/message-safety-mode.env`; менять его и перезапускать stack может только fixed helper, разрешённый `deploy` через exact-argument sudoers;
- API container получает read-only
`/etc/han-chat/message-safety-mode.env` с host contract
`root:han-message-safety 0640` и GID `10001`; менять его и перезапускать
stack может только fixed helper, разрешённый `deploy` через exact-argument
sudoers;
- экспортирует traces/logs в `otel-collector`;
- таймауты: POST check 5 s, GET task 2 s, file scan 60 s (см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), переменные `MESSAGE_SAFETY_*`).
@@ -402,6 +430,13 @@ Generated config, PID/socket, cache и logs без retention размещают
ограниченных tmpfs. Один writable volume не объединяет config/executable со
state.
Local OTEL queue на каждой VM использует отдельный persistent named volume.
Перед collector запускается `otel-queue-init`: ограниченный one-shot service
выставляет mount root в `10001:10001 0700`, не имеет сети/secrets и завершается
до старта collector. Collector остаётся non-root; запуск collector от root,
`chmod 0777` и надежда на автоматическое наследование ownership из image
запрещены.
## Переменные окружения
Каждая VM имеет свой allow-listed non-secret env manifest. Секреты доставляются отдельными service files согласно arch-04/06; общий env/secret bundle двух VM запрещён.
@@ -478,6 +513,9 @@ state.
- document/entity/domain/member fields проверяются в `bitrix-sync`; local app/event handler для CRM sync не используется;
- кэширование отключено; source IP/body/method/rate limits применяются до private proxy; IP rejects экспортируются в telemetry без IP label;
- при sync disabled/cutover route закрыт либо возвращает retryable `503`, а не `202 ignored`;
- `BITRIX_SYNC_ENABLED=false` требует активный edge allow-list только с
`deny all;`; наличие `allow` при disabled и отсутствие reviewed `allow` при
enabled являются preflight error;
- `/internal/sync/v1/*` не публикуется наружу (только internal network + `BITRIX_SYNC_SERVICE_TOKEN`).
## Rate limits и защита от abuse
@@ -545,6 +583,14 @@ WAF не заменяет обязательные лимиты, валидац
Наружу через `nginx` публикуются только health endpoint, которые нужны Bitrix24 install/callback validation или внешнему мониторингу. Internal services (`message-safety`, internal `bitrix-sync`, Redis, otel) проверяются только из Docker/VPC-сети.
Healthcheck считается валидным только после запуска внутри exact production
image digest с теми же `user`, `read_only`, mounts и dropped capabilities.
Команда не может предполагать наличие `curl`, `wget`, shell или package,
которого нет в image/SBOM; `ExitCode 127` является дефектом release, а не
`unhealthy` приложения. Для minimal nginx предпочтителен гарантированно
доступный native binary/config test либо специально включённый и проверенный
client.
Healthcheck не копируется между разными commands одного image без проверки.
Если updater не запускает daemon, daemon-socket healthcheck для него
отключается. Freshness данных контролируется отдельной метрикой/проверкой
@@ -558,7 +604,7 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
ВМ2 запускается в порядке:
1. `redis-safety` и local `otel-collector`;
1. `redis-safety`, `otel-queue-init`, затем local `otel-collector`;
2. `freshclam`, затем `clamd` до состояния healthy;
3. Message Safety API/worker и `bitrix-sync`;
4. nginx — последним, после успешного config test;
@@ -566,7 +612,7 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
ВМ1 запускается в порядке:
1. Redis и `otel-collector`;
1. Redis, `otel-queue-init`, затем `otel-collector`;
2. Keycloak, `sms-service`/worker, `api-backend` и `bitrix-local-app` с их
readiness-зависимостями;
3. notification expire/cleanup workers после готовности `api-backend`;
@@ -574,7 +620,11 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
Порядок rollout SMS подробнее задаёт module-11/module-10. Зависимости запуска не образуют цикл: Keycloak стартует при недоступном `sms-service`; это блокирует только новые real-mode orders, а verify уже active challenges продолжается по snapshot.
`depends_on` не заменяет проверку готовности. Сервисы должны уметь ждать зависимости или корректно деградировать. `api-backend` может стать ready для read API до ВМ2, но send endpoint обязан fail-closed при недоступной требуемой capability Safety.
`depends_on` не заменяет проверку готовности. Для edge применяются три слоя:
ordered start, readiness dependencies и post-ready config test/reload с
production DNS names. Сервисы должны уметь ждать зависимости или корректно
деградировать. `api-backend` может стать ready для read API до ВМ2, но send
endpoint обязан fail-closed при недоступной требуемой capability Safety.
## Развёртывание на ВМ1 и ВМ2
@@ -584,19 +634,30 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
4. Public nginx ВМ2 публикует `80/443`; `80` используется только для ACME/redirect, `443` — только exact CRM webhook. Private `8443` разрешён только от SG ВМ1 и ops для Message Safety/internal access.
5. Deploy/cutover ВМ2 не требует изменения public routes ВМ1. Для `bitrix-sync` rollback закрывает webhook routes на nginx ВМ2 либо возвращает retryable `503`, останавливает claims и сохраняет durable tasks/mapping; возврат к фиктивному `202 ignored` запрещён.
Host firewall обеих VM учитывает post-DNAT semantics `DOCKER-USER`: policy
published nginx ports сопоставляет original host destination через conntrack,
как определено в arch-06. Проверка только UFW INPUT или текущего container
`--dport` не является достаточной.
Перед первым `up` и после смены любого image digest обязательны permission
gates:
1. проверить LF/shebang и root ownership release-артефактов;
2. сверить UID/GID контейнеров с owner/mode host bind mounts, secret files и
2. проверить manifest modes и executable bit scripts/preflight/hooks; blanket
`F644` для всего release запрещён;
3. сверить UID/GID контейнеров с owner/mode host bind mounts, secret files и
TLS staging;
3. проверить доступ целевого UID и отказ постороннему UID;
4. выполнить PEM parse/key-match и Compose render;
5. запустить image-native config test/entrypoint с production hardening и
4. проверить доступ целевого UID и отказ постороннему UID, включая traverse
parent directories;
5. выполнить/проверить one-shot ownership init для non-root named volumes;
6. выполнить PEM parse/key-match и Compose render;
7. запустить image-native config test/entrypoint/healthcheck с production hardening и
временными test-only upstream values;
6. выдать migration-role только необходимые временные cross-schema grants,
8. выдать migration-role только необходимые временные cross-schema grants,
выполнить Alembic и отозвать grants владельцем;
7. после старта проверить отсутствие permission/restart loops, реальные
9. после readiness upstream повторить edge config test/reload с production
Docker DNS names;
10. после старта проверить отсутствие permission/restart loops, реальные
healthchecks и freshness updater data.
Неуспех gate исправляется в ownership, mount/entrypoint contract или DB grants.
+15 -1
View File
@@ -201,9 +201,17 @@ worker.lease_seconds=90
Runtime policy хранится в версионированной `message_safety.config_versions`, а не в `.env` и не в `han_app.app_settings`. Сюда входят task lease/deadline/attempts, internal rate/pending limits, retention/cache TTL, URL/DNS pipeline limits, ClamAV policy timeout/signature age и enabled file MIME/size policy. Для ClamAV schema допускает возраст сигнатур не более `720` часов (30 дней), seed `max_signature_age_hours` равен `240` часам (10 дней). Полный schema/seed/activation contract — module-05 §10.1 и §15.
Seed, JSON Schema и referenced artifacts входят в immutable Message Safety
image. Их изменение требует одновременно нового pinned image digest и новой
монотонной config version через config-admin job. Нельзя активировать config,
который старый runtime image не валидирует, оставлять старый image как
автоматический rollback после активации несовместимой schema или редактировать
active row на месте. Rollback выполняется новой config version, совместимой с
выбранным image; retired version повторно не активируется.
`han_app.app_settings:chat.attachments.*` остаётся бизнес-настройкой api-backend. Message Safety не получает cross-schema read к `han_app`; файл допускается только при пересечении business allow-list, active safety policy и immutable detector manifest. Active policy может сузить manifest, но не добавить parser и не увеличить hard limit.
В env Message Safety остаются только bootstrap/topology/capacity (`APP_ENV`, worker concurrency, DNS resolver, ClamAV/S3/OTLP endpoints); credentials доставляются secret files. Rules/detector versions вычисляются/проверяются по immutable artifacts. Emergency MOCK остаётся в отдельном root-owned mode file и намеренно не переносится в БД.
В env Message Safety остаются только bootstrap/topology/capacity (`APP_ENV`, worker concurrency, DNS resolver, ClamAV/S3/OTLP endpoints); credentials доставляются secret files. Rules/detector versions вычисляются/проверяются по immutable artifacts. Emergency MOCK остаётся в отдельном read-only bind file `root:han-message-safety 0640` с dedicated GID контейнера и намеренно не переносится в БД.
---
@@ -433,6 +441,12 @@ Production использует отдельные allow-listed env manifests В
При `false` контейнер остаётся live, `/health/ready` возвращает `503 sync_disabled`, worker/reconciliation не claim-ят работу. Public webhook не должен безусловно подтверждать событие как обработанное: до cutover endpoint закрывается на edge либо возвращает retryable `503`. Это не влияет на чат Open Lines.
Feature flag, edge allow-list и readiness образуют единый fail-closed
инвариант: при `false` активный nginx allow-list содержит только `deny all;`;
при `true` требуется хотя бы один reviewed source CIDR и готовый receiver.
Непустой allow-list при disabled, пустой allow-list при enabled или
расхождение route/readiness являются preflight error.
### `bitrix_sync.settings`
Versioned hot settings содержат batch size/wait, claim size, lease TTL, portal limiter refill/burst, max in-flight, retry base/max/horizon, Contact/alert reconciliation intervals, пороги всплеска Contact, восстановленных без webhook, и IDs/стадии/поля/SLA smart process. Новая версия активируется только после полной type/range/cross-field validation; невалидная версия не заменяет последнюю рабочую.
@@ -20,6 +20,13 @@
- Изменение deployment, VM topology, network exposure, volumes, Linux capabilities, OS/sudo-прав или способа доставки секретов требует impact analysis по arch-06.
- Агент не добавляет `deploy` в группу `docker` и не расширяет sudo wildcard-командами. Новое право оформляется как конкретная операция над конкретным systemd-unit с review и rollback.
- Production compose, systemd-units, deploy scripts и secret mappings остаются root-owned и недоступны `deploy` на запись.
- Release содержит version-controlled manifest owner/group/mode; blanket
`F644` sync запрещён, executable scripts/hooks/preflight сохраняют `+x`.
- Изменение image digest повторяет runtime-проверки exact image: UID/GID,
entrypoint, writable paths, healthcheck binaries и one-shot init jobs.
- Изменение bind permissions, feature flag/edge allow-list, published ports
или systemd oneshot helper требует positive/negative production-like test,
а не только review YAML/shell.
- Для private/no-egress VM допустим временный bootstrap с SSH из trusted ops CIDR и ограниченным egress для пакетов/образов.
- Раскатка private/no-egress VM считается незавершённой, пока не выполнен lockdown: public ingress/SSH и общий egress закрыты, private access проверен, а недоступность снаружи зафиксирована.
- Повторное открытие ingress/egress после lockdown — документированная break-glass операция с обязательным возвратом в lockdown, а не штатный способ деплоя.
@@ -101,6 +108,16 @@ Raw OTP запрещено хранить в открытом виде: это
- сервис запускается в root Docker Compose своей VM; CI отдельно валидирует оба projects и отсутствие cross-host `depends_on`/Docker DNS;
- remote Message Safety contract tests покрывают v2 `202 + Location + Retry-After`, sticky final result, terminal failed `503`, `409` invariant mapping и legacy v1 migration adapter;
- container проверен по arch-06: non-root, `no-new-privileges`, capabilities, read-only filesystem/writable paths, volumes, networks и resource limits;
- release manifest проверяет executable modes; bind files читаются реальным
container UID/GID, named volumes подготовлены ограниченным init-job;
- healthcheck выполнен внутри exact pinned digest, без несуществующих
вспомогательных binaries;
- published Docker ports проверены через live `DOCKER-USER` counters с учётом
DNAT; обновлённый active oneshot helper применён explicit restart;
- hooks/timers имеют корректный exit code и пустой stderr при успехе;
- feature flags, edge allow-lists и service readiness согласованы fail-closed;
- изменение embedded seed/schema выпущено как совместимая пара нового image
digest и новой монотонной config version с проверенным rollback;
- изменение прав `deploy`, systemd, network exposure, capabilities, volumes или secret delivery отражено в arch-06 и deployment runbook;
- для private/no-egress VM выполнен и зафиксирован bootstrap→lockdown checklist;
- worker, указанный в Compose/runbook, имеет реально зарегистрированный entrypoint в image; deployment не может заранее выдумывать имя команды;
@@ -217,6 +217,15 @@ AllowUsers deploy admin tunnel
`deploy` не запускает `docker`, `docker compose` или произвольные root-скрипты через sudo. Docker Compose запускается root-owned systemd-юнитом или root-owned deployment helper с фиксированным интерфейсом.
Обновление executable, config или helper, используемого уже активным
`Type=oneshot` unit с `RemainAfterExit=yes`, требует явного
`systemctl restart <unit>`. `enable --now` включает unit и стартует только
неактивный unit, но не применяет новый helper к уже active/exited unit.
Rollout проверяет фактический live state после restart: созданные firewall
rules, freshness materialized secrets, owner/mode файлов и exit status
последнего запуска. Один `ActiveState=active` не доказывает применение новой
версии.
Sudoers хранится только в `/etc/sudoers.d/deploy` и проверяется через `visudo`. `/etc/sudoers` напрямую не редактируется.
Разрешения перечисляют полные команды и конкретные unit names без wildcard. Принципиальный пример:
@@ -263,7 +272,16 @@ Root-owned и недоступны `deploy` на запись:
- отсутствуют symlink/path traversal;
- compose config прошёл валидацию;
- image reference pinned по version/digest;
- миграции и rollout соответствуют release manifest.
- миграции и rollout соответствуют release manifest;
- release manifest задаёт owner/group/mode для каждого класса файлов:
data/config `0644` или строже, secrets отдельно, executable scripts,
preflight, hooks и helpers — `0755`/`0750` по назначению;
- перед активацией выполняется `test -x` для каждого executable из manifest.
Глобальный `rsync --chmod=F644`, рекурсивный `chmod` или иной blanket mode,
снимающий executable bit со scripts/hooks/preflight, запрещён. Права
восстанавливаются из version-controlled manifest или явными `install -m`
для каждого класса артефактов, а не после первой ошибки запуска.
Если такого валидатора пока нет, compose/unit changes выполняет `admin`, а `deploy` ограничивается запуском уже подготовленного релиза. Выдавать `deploy` запись в production compose — не допустимая замена автоматизации.
@@ -313,9 +331,14 @@ Wildcard, временный `NOPASSWD: ALL` и включение в `docker` g
Значения `uid`, `gid` и `mode` в Compose file secrets нельзя считать
security boundary: Docker Compose при bind-backed secret может их игнорировать.
Фактические owner/mode задаются host-side materializer'ом и проверяются через
`stat` и негативный тест от постороннего UID. Предупреждение Compose об
игнорировании этих атрибутов не подавляется и не трактуется как подтверждение
прав.
`stat`, позитивный read-test от фактического container UID/GID и негативный
тест от постороннего UID. Проверяется также execute/traverse permission всех
parent directories. `root:root 0600` нельзя bind-mount в процесс, работающий
не от root: для несекретного control file используется dedicated host group,
совпадающий с primary GID контейнера, и минимальный режим `0640`; secrets
получают эквивалентный минимальный ACL/group contract. Предупреждение Compose
об игнорировании этих атрибутов не подавляется и не трактуется как
подтверждение прав.
Структурированные секреты валидируются до старта потребителя. Для PEM это
означает проверку парсинга certificate/private key, отсутствие повторного
@@ -382,7 +405,7 @@ revision, а не ручным `stamp` или правкой production DB.
| Путь | Владелец / режим | Назначение |
|---|---|---|
| `/opt/han-chat/releases/<version>` | `root:root`, `0755`/файлы `0644` | immutable release |
| `/opt/han-chat/releases/<version>` | `root:root`; каталоги `0755`, data/config `0644`, executable по manifest `0755/0750` | immutable release |
| `/opt/han-chat/current` | `root:root` | active release link; меняет только deployment helper/admin |
| `/var/lib/han-deploy/incoming` | `deploy:deploy`, `0750` | загрузка неактивированных артефактов |
| `/etc/han` | `root:root`, `0750` или строже | конфигурация и secret mappings |
@@ -455,6 +478,27 @@ non-root + `read_only`, даже если сам daemon способен раб
writable-path inventory, healthcheck semantics и фактический UID/GID считаются
частью security contract образа.
### Ownership persistent named volumes
Новый Docker named volume нельзя считать writable для non-root process:
начальный owner часто `root:root`, даже если target path в image принадлежит
service UID. Persistent volume до запуска потребителя подготавливает
идемпотентный one-shot init service:
- использует уже утверждённый pinned image с необходимыми `sh/chown/chmod`, а
не непроверенный floating utility image;
- запускается с `user: 0:0`, `read_only: true`, `cap_drop: [ALL]` и возвращает
только `CHOWN`/`FOWNER`, если они действительно нужны;
- не имеет сети (`network_mode: none`) и доступа к secrets;
- меняет owner/mode только mount root конкретного volume, без recursive chown
чужого state;
- завершается с кодом `0`, а consumer зависит от
`condition: service_completed_successfully`;
- безопасно повторяется после recreate и отдельно проверяется в preflight.
Запуск основного collector/service от root, `chmod 0777` и ручная правка
`/var/lib/docker/volumes` на host запрещены как способы исправления ownership.
### TLS для non-root edge
Root-only дерево ACME/Certbot не монтируется целиком в non-root nginx и не
@@ -463,7 +507,11 @@ Root-only дерево ACME/Certbot не монтируется целиком
`han-nginx-tls` (канонический GID `11001`); nginx получает этот каталог
read-only. Renewal hook сначала обновляет staged files, затем выполняет полный
config test и только после успеха отправляет reload. Права и соответствие
certificate/key проверяются preflight.
certificate/key проверяются preflight. Успешный hook обязан завершаться с
кодом `0` и пустым stderr: benign output `nginx -t` и progress Compose
перехватываются/подавляются на success path, но полностью выдаются в stderr
при ненулевом exit code. Иначе Certbot/оркестратор может пометить успешный
renewal как hook error.
### Daemon и updater как разные security-профили
@@ -500,6 +548,23 @@ capability.
Default policy для ingress — deny. Разрешение задаёт source, destination, protocol, port и назначение. Правила «вся private network на все порты» запрещены.
`DOCKER-USER` обрабатывает forwarded packet после Docker DNAT. Поэтому policy
для published host ports сопоставляет original destination через
`-m conntrack --ctorigdstport <HOST_PORT>` (или эквивалент nftables), а не
текущий `--dport`, который уже может быть container port. Для каждого
разрешённого host port обязательны:
- positive external/private probe и рост counter именно allow rule;
- negative probe неразрешённого source/port и рост deny counter;
- проверка `iptables -S`/nft ruleset после setup, restart firewall unit,
restart Docker и reboot;
- запрет считать UFW INPUT counter доказательством фильтрации published
Docker port: такой трафик может обходить INPUT.
Firewall setup обязан обновлять helper и явно перезапускать active oneshot
unit; наличие нового текста helper без изменения live rules является
неуспешным rollout.
Cloud SG для managed PostgreSQL разрешает TLS-подключения только от VM/SG сервисов, которым нужна соответствующая схема. PostgreSQL, Redis, OTLP receivers, admin UI и internal API не публикуются в интернет.
### Egress
@@ -561,9 +626,13 @@ Fail2ban обязателен для SSH, временно или постоян
- root/password SSH отключены после проверки key access;
- `deploy` не состоит в `docker` и имеет только конкретные systemd-команды;
- production-файлы root-owned и недоступны `deploy` на запись;
- release manifest сохраняет executable modes; preflight/hooks запускаются
напрямую без обхода через `bash`;
- каждый контейнер проверен по hardening baseline;
- для каждого image digest проверены entrypoint, UID/GID и полный inventory
writable paths;
- bind files проходят positive read-test от container UID/GID и negative test
от постороннего UID; named volumes подготовлены ограниченным init-job;
- секреты разделены по сервисам/VM и отсутствуют в обычном `.env`;
- bind-backed secrets и staged TLS проверены по фактическим owner/mode и
содержимому, а не только по декларации Compose;
@@ -571,7 +640,13 @@ Fail2ban обязателен для SSH, временно или постоян
grants выданы и отозваны владельцем;
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
updater freshness контролируется отдельным сигналом;
- healthcheck-команда и все её binaries подтверждены внутри exact pinned
digest; отсутствие `curl`/`wget` не обнаруживается впервые в production;
- внутренние ports недоступны извне;
- live `DOCKER-USER` rules проверены по original host ports после DNAT и
переживают Docker restart/reboot;
- обновлённые oneshot helpers применены explicit restart, а renewal hooks
имеют пустой stderr при успехе;
- backup/restore и rollback проверены в объёме релиза;
- для private/no-egress VM завершён и зафиксирован lockdown;
- проверена недоступность внешних портов и неразрешённого egress;
+22 -1
View File
@@ -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
+9 -1
View File
@@ -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
+8
View File
@@ -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 510 ГБ; уточнить 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
+54 -1
View File
@@ -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 минимум дважды в год.