Внесены правки в документацию
This commit is contained in:
@@ -77,5 +77,12 @@
|
|||||||
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
|
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
|
||||||
- Новый env или ключ `app_settings` → arch-04.
|
- Новый env или ключ `app_settings` → arch-04.
|
||||||
- Новая VM, изменение сетевой доступности, прав `deploy`, sudo/systemd, capabilities, volumes или способа доставки секретов → arch-06 (+ arch-03/arch-04 и deployment runbook).
|
- Новая 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-*.
|
- Новый термин / enum → arch-00, затем поиск по arch-*.
|
||||||
- Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в 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.
|
- **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.
|
- Между 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`
|
### Структура Compose через `include`
|
||||||
|
|
||||||
Каждый сервис описывается в собственном `docker-compose.yml` и подключается в root-файл своей VM директивой `include`:
|
Каждый сервис описывается в собственном `docker-compose.yml` и подключается в root-файл своей VM директивой `include`:
|
||||||
@@ -119,7 +128,13 @@ Root Compose ВМ2 включает собственный nginx с public/priva
|
|||||||
logs и persistent state;
|
logs и persistent state;
|
||||||
- отдельный volume/tmpfs для каждого writable path с явными
|
- отдельный volume/tmpfs для каждого writable path с явными
|
||||||
`uid/gid/mode`, размером и mount flags;
|
`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 semantics: успешный one-shot exit не должен превращаться в
|
||||||
бесконечный restart/download loop.
|
бесконечный restart/download loop.
|
||||||
|
|
||||||
@@ -131,6 +146,8 @@ mount, а не `read_only: false`, root, `privileged` или broad capabilities.
|
|||||||
Compose file secrets с bind-backed `file:` могут игнорировать декларативные
|
Compose file secrets с bind-backed `file:` могут игнорировать декларативные
|
||||||
`uid/gid/mode`. Их фактические host permissions создаёт secret materializer;
|
`uid/gid/mode`. Их фактические host permissions создаёт secret materializer;
|
||||||
rollout проверяет owner/mode из контейнера и с host, не полагаясь на YAML.
|
rollout проверяет owner/mode из контейнера и с host, не полагаясь на YAML.
|
||||||
|
`root:root 0600` несовместим с non-root consumer; dedicated group/ACL должен
|
||||||
|
совпадать с фактическим container GID и не давать доступ постороннему UID.
|
||||||
|
|
||||||
При сборке images необхоидмо добавлять нормализацию CRLF→LF
|
При сборке images необхоидмо добавлять нормализацию CRLF→LF
|
||||||
(например, RUN sed -i 's/\r$//' <directory/service-name> \
|
(например, 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. До запуска
|
- pre-start config test выполняет реальный image entrypoint. До запуска
|
||||||
upstream-контейнеров их host variables временно подменяются loopback IP
|
upstream-контейнеров их host variables временно подменяются loopback IP
|
||||||
только в test container; production Compose сохраняет service DNS names;
|
только в 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), «Принципы безопасности»):
|
- **политика HTTP/HTTPS по доменам** (каноническое правило — [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности»):
|
||||||
- **веб-домен** (frontend, SPA, статика): `listen 80` допускается **только** для безусловного редиректа `301`/`308` на HTTPS; обработка бизнес-логики по HTTP запрещена;
|
- **веб-домен** (frontend, SPA, статика): `listen 80` допускается **только** для безусловного редиректа `301`/`308` на HTTPS; обработка бизнес-логики по HTTP запрещена;
|
||||||
- **API-домен** (если выделен отдельный host, напр. `api.example.ru`): **не** слушает порт `80`; только `listen 443 ssl`; HTTP-запросы к API-домену недоступны;
|
- **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;
|
- runtime role читает immutable active `message_safety.config_versions`; создавать/активировать config может только отдельный migration/config-admin job;
|
||||||
- использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
|
- использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
|
||||||
- запускает async workers для file scan из S3-quarantine;
|
- запускает 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`;
|
- экспортирует 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_*`).
|
- таймауты: 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 со
|
ограниченных tmpfs. Один writable volume не объединяет config/executable со
|
||||||
state.
|
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 запрещён.
|
Каждая 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 не используется;
|
- 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;
|
- кэширование отключено; source IP/body/method/rate limits применяются до private proxy; IP rejects экспортируются в telemetry без IP label;
|
||||||
- при sync disabled/cutover route закрыт либо возвращает retryable `503`, а не `202 ignored`;
|
- при 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`).
|
- `/internal/sync/v1/*` не публикуется наружу (только internal network + `BITRIX_SYNC_SERVICE_TOKEN`).
|
||||||
|
|
||||||
## Rate limits и защита от abuse
|
## 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-сети.
|
Наружу через `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 без проверки.
|
Healthcheck не копируется между разными commands одного image без проверки.
|
||||||
Если updater не запускает daemon, daemon-socket healthcheck для него
|
Если updater не запускает daemon, daemon-socket healthcheck для него
|
||||||
отключается. Freshness данных контролируется отдельной метрикой/проверкой
|
отключается. Freshness данных контролируется отдельной метрикой/проверкой
|
||||||
@@ -558,7 +604,7 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
|
|||||||
|
|
||||||
ВМ2 запускается в порядке:
|
ВМ2 запускается в порядке:
|
||||||
|
|
||||||
1. `redis-safety` и local `otel-collector`;
|
1. `redis-safety`, `otel-queue-init`, затем local `otel-collector`;
|
||||||
2. `freshclam`, затем `clamd` до состояния healthy;
|
2. `freshclam`, затем `clamd` до состояния healthy;
|
||||||
3. Message Safety API/worker и `bitrix-sync`;
|
3. Message Safety API/worker и `bitrix-sync`;
|
||||||
4. nginx — последним, после успешного config test;
|
4. nginx — последним, после успешного config test;
|
||||||
@@ -566,7 +612,7 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
|
|||||||
|
|
||||||
ВМ1 запускается в порядке:
|
ВМ1 запускается в порядке:
|
||||||
|
|
||||||
1. Redis и `otel-collector`;
|
1. Redis, `otel-queue-init`, затем `otel-collector`;
|
||||||
2. Keycloak, `sms-service`/worker, `api-backend` и `bitrix-local-app` с их
|
2. Keycloak, `sms-service`/worker, `api-backend` и `bitrix-local-app` с их
|
||||||
readiness-зависимостями;
|
readiness-зависимостями;
|
||||||
3. notification expire/cleanup workers после готовности `api-backend`;
|
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.
|
Порядок 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
|
## Развёртывание на ВМ1 и ВМ2
|
||||||
|
|
||||||
@@ -584,20 +634,31 @@ 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.
|
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` запрещён.
|
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
|
Перед первым `up` и после смены любого image digest обязательны permission
|
||||||
gates:
|
gates:
|
||||||
|
|
||||||
1. проверить LF/shebang и root ownership release-артефактов;
|
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;
|
TLS staging;
|
||||||
3. проверить доступ целевого UID и отказ постороннему UID;
|
4. проверить доступ целевого UID и отказ постороннему UID, включая traverse
|
||||||
4. выполнить PEM parse/key-match и Compose render;
|
parent directories;
|
||||||
5. запустить image-native config test/entrypoint с production hardening и
|
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;
|
временными test-only upstream values;
|
||||||
6. выдать migration-role только необходимые временные cross-schema grants,
|
8. выдать migration-role только необходимые временные cross-schema grants,
|
||||||
выполнить Alembic и отозвать grants владельцем;
|
выполнить Alembic и отозвать grants владельцем;
|
||||||
7. после старта проверить отсутствие permission/restart loops, реальные
|
9. после readiness upstream повторить edge config test/reload с production
|
||||||
healthchecks и freshness updater data.
|
Docker DNS names;
|
||||||
|
10. после старта проверить отсутствие permission/restart loops, реальные
|
||||||
|
healthchecks и freshness updater data.
|
||||||
|
|
||||||
Неуспех gate исправляется в ownership, mount/entrypoint contract или DB grants.
|
Неуспех gate исправляется в ownership, mount/entrypoint contract или DB grants.
|
||||||
Временное ослабление `read_only`, запуск root, broad chmod, добавление в
|
Временное ослабление `read_only`, запуск root, broad chmod, добавление в
|
||||||
|
|||||||
@@ -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.
|
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.
|
`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.
|
При `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`
|
### `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; невалидная версия не заменяет последнюю рабочую.
|
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.
|
- Изменение deployment, VM topology, network exposure, volumes, Linux capabilities, OS/sudo-прав или способа доставки секретов требует impact analysis по arch-06.
|
||||||
- Агент не добавляет `deploy` в группу `docker` и не расширяет sudo wildcard-командами. Новое право оформляется как конкретная операция над конкретным systemd-unit с review и rollback.
|
- Агент не добавляет `deploy` в группу `docker` и не расширяет sudo wildcard-командами. Новое право оформляется как конкретная операция над конкретным systemd-unit с review и rollback.
|
||||||
- Production compose, systemd-units, deploy scripts и secret mappings остаются root-owned и недоступны `deploy` на запись.
|
- 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 допустим временный bootstrap с SSH из trusted ops CIDR и ограниченным egress для пакетов/образов.
|
||||||
- Раскатка private/no-egress VM считается незавершённой, пока не выполнен lockdown: public ingress/SSH и общий egress закрыты, private access проверен, а недоступность снаружи зафиксирована.
|
- Раскатка private/no-egress VM считается незавершённой, пока не выполнен lockdown: public ingress/SSH и общий egress закрыты, private access проверен, а недоступность снаружи зафиксирована.
|
||||||
- Повторное открытие ingress/egress после lockdown — документированная break-glass операция с обязательным возвратом в lockdown, а не штатный способ деплоя.
|
- Повторное открытие ingress/egress после lockdown — документированная break-glass операция с обязательным возвратом в lockdown, а не штатный способ деплоя.
|
||||||
@@ -101,6 +108,16 @@ Raw OTP запрещено хранить в открытом виде: это
|
|||||||
- сервис запускается в root Docker Compose своей VM; CI отдельно валидирует оба projects и отсутствие cross-host `depends_on`/Docker DNS;
|
- сервис запускается в 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;
|
- 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;
|
- 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;
|
- изменение прав `deploy`, systemd, network exposure, capabilities, volumes или secret delivery отражено в arch-06 и deployment runbook;
|
||||||
- для private/no-egress VM выполнен и зафиксирован bootstrap→lockdown checklist;
|
- для private/no-egress VM выполнен и зафиксирован bootstrap→lockdown checklist;
|
||||||
- worker, указанный в Compose/runbook, имеет реально зарегистрированный entrypoint в image; deployment не может заранее выдумывать имя команды;
|
- 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 с фиксированным интерфейсом.
|
`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` напрямую не редактируется.
|
Sudoers хранится только в `/etc/sudoers.d/deploy` и проверяется через `visudo`. `/etc/sudoers` напрямую не редактируется.
|
||||||
|
|
||||||
Разрешения перечисляют полные команды и конкретные unit names без wildcard. Принципиальный пример:
|
Разрешения перечисляют полные команды и конкретные unit names без wildcard. Принципиальный пример:
|
||||||
@@ -263,7 +272,16 @@ Root-owned и недоступны `deploy` на запись:
|
|||||||
- отсутствуют symlink/path traversal;
|
- отсутствуют symlink/path traversal;
|
||||||
- compose config прошёл валидацию;
|
- compose config прошёл валидацию;
|
||||||
- image reference pinned по version/digest;
|
- 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 — не допустимая замена автоматизации.
|
Если такого валидатора пока нет, compose/unit changes выполняет `admin`, а `deploy` ограничивается запуском уже подготовленного релиза. Выдавать `deploy` запись в production compose — не допустимая замена автоматизации.
|
||||||
|
|
||||||
@@ -313,9 +331,14 @@ Wildcard, временный `NOPASSWD: ALL` и включение в `docker` g
|
|||||||
Значения `uid`, `gid` и `mode` в Compose file secrets нельзя считать
|
Значения `uid`, `gid` и `mode` в Compose file secrets нельзя считать
|
||||||
security boundary: Docker Compose при bind-backed secret может их игнорировать.
|
security boundary: Docker Compose при bind-backed secret может их игнорировать.
|
||||||
Фактические owner/mode задаются host-side materializer'ом и проверяются через
|
Фактические 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 это
|
Структурированные секреты валидируются до старта потребителя. Для PEM это
|
||||||
означает проверку парсинга certificate/private key, отсутствие повторного
|
означает проверку парсинга 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 |
|
| `/opt/han-chat/current` | `root:root` | active release link; меняет только deployment helper/admin |
|
||||||
| `/var/lib/han-deploy/incoming` | `deploy:deploy`, `0750` | загрузка неактивированных артефактов |
|
| `/var/lib/han-deploy/incoming` | `deploy:deploy`, `0750` | загрузка неактивированных артефактов |
|
||||||
| `/etc/han` | `root:root`, `0750` или строже | конфигурация и secret mappings |
|
| `/etc/han` | `root:root`, `0750` или строже | конфигурация и secret mappings |
|
||||||
@@ -455,6 +478,27 @@ non-root + `read_only`, даже если сам daemon способен раб
|
|||||||
writable-path inventory, healthcheck semantics и фактический UID/GID считаются
|
writable-path inventory, healthcheck semantics и фактический UID/GID считаются
|
||||||
частью security contract образа.
|
частью 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
|
### TLS для non-root edge
|
||||||
|
|
||||||
Root-only дерево ACME/Certbot не монтируется целиком в non-root nginx и не
|
Root-only дерево ACME/Certbot не монтируется целиком в non-root nginx и не
|
||||||
@@ -463,7 +507,11 @@ Root-only дерево ACME/Certbot не монтируется целиком
|
|||||||
`han-nginx-tls` (канонический GID `11001`); nginx получает этот каталог
|
`han-nginx-tls` (канонический GID `11001`); nginx получает этот каталог
|
||||||
read-only. Renewal hook сначала обновляет staged files, затем выполняет полный
|
read-only. Renewal hook сначала обновляет staged files, затем выполняет полный
|
||||||
config test и только после успеха отправляет reload. Права и соответствие
|
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-профили
|
### Daemon и updater как разные security-профили
|
||||||
|
|
||||||
@@ -500,6 +548,23 @@ capability.
|
|||||||
|
|
||||||
Default policy для ingress — deny. Разрешение задаёт source, destination, protocol, port и назначение. Правила «вся private network на все порты» запрещены.
|
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 не публикуются в интернет.
|
Cloud SG для managed PostgreSQL разрешает TLS-подключения только от VM/SG сервисов, которым нужна соответствующая схема. PostgreSQL, Redis, OTLP receivers, admin UI и internal API не публикуются в интернет.
|
||||||
|
|
||||||
### Egress
|
### Egress
|
||||||
@@ -561,9 +626,13 @@ Fail2ban обязателен для SSH, временно или постоян
|
|||||||
- root/password SSH отключены после проверки key access;
|
- root/password SSH отключены после проверки key access;
|
||||||
- `deploy` не состоит в `docker` и имеет только конкретные systemd-команды;
|
- `deploy` не состоит в `docker` и имеет только конкретные systemd-команды;
|
||||||
- production-файлы root-owned и недоступны `deploy` на запись;
|
- production-файлы root-owned и недоступны `deploy` на запись;
|
||||||
|
- release manifest сохраняет executable modes; preflight/hooks запускаются
|
||||||
|
напрямую без обхода через `bash`;
|
||||||
- каждый контейнер проверен по hardening baseline;
|
- каждый контейнер проверен по hardening baseline;
|
||||||
- для каждого image digest проверены entrypoint, UID/GID и полный inventory
|
- для каждого image digest проверены entrypoint, UID/GID и полный inventory
|
||||||
writable paths;
|
writable paths;
|
||||||
|
- bind files проходят positive read-test от container UID/GID и negative test
|
||||||
|
от постороннего UID; named volumes подготовлены ограниченным init-job;
|
||||||
- секреты разделены по сервисам/VM и отсутствуют в обычном `.env`;
|
- секреты разделены по сервисам/VM и отсутствуют в обычном `.env`;
|
||||||
- bind-backed secrets и staged TLS проверены по фактическим owner/mode и
|
- bind-backed secrets и staged TLS проверены по фактическим owner/mode и
|
||||||
содержимому, а не только по декларации Compose;
|
содержимому, а не только по декларации Compose;
|
||||||
@@ -571,7 +640,13 @@ Fail2ban обязателен для SSH, временно или постоян
|
|||||||
grants выданы и отозваны владельцем;
|
grants выданы и отозваны владельцем;
|
||||||
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
|
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
|
||||||
updater freshness контролируется отдельным сигналом;
|
updater freshness контролируется отдельным сигналом;
|
||||||
|
- healthcheck-команда и все её binaries подтверждены внутри exact pinned
|
||||||
|
digest; отсутствие `curl`/`wget` не обнаруживается впервые в production;
|
||||||
- внутренние ports недоступны извне;
|
- внутренние ports недоступны извне;
|
||||||
|
- live `DOCKER-USER` rules проверены по original host ports после DNAT и
|
||||||
|
переживают Docker restart/reboot;
|
||||||
|
- обновлённые oneshot helpers применены explicit restart, а renewal hooks
|
||||||
|
имеют пустой stderr при успехе;
|
||||||
- backup/restore и rollback проверены в объёме релиза;
|
- backup/restore и rollback проверены в объёме релиза;
|
||||||
- для private/no-egress VM завершён и зафиксирован lockdown;
|
- для private/no-egress VM завершён и зафиксирован lockdown;
|
||||||
- проверена недоступность внешних портов и неразрешённого egress;
|
- проверена недоступность внешних портов и неразрешённого egress;
|
||||||
|
|||||||
@@ -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.
|
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 конфигурации.
|
Запрос вне 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
|
## 3. Upstreams
|
||||||
@@ -51,6 +55,11 @@ Nginx ВМ2 имеет независимые server blocks:
|
|||||||
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
|
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
|
||||||
- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops.
|
- 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 |
|
| Path | Local upstream | Caller |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 |
|
| `/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` и
|
master-процессу `docker compose kill -s HUP nginx`. Bare-команды `nginx -t` и
|
||||||
`nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся
|
`nginx -s reload` запрещены: контейнер read-only, а рабочие config/PID находятся
|
||||||
в `/tmp`. При ошибке остаётся старый worker/config/cert и срабатывает alert.
|
в `/tmp`. При ошибке остаётся старый worker/config/cert и срабатывает alert.
|
||||||
|
Успешный deploy/renew hook возвращает `0` с пустым stderr: вывод успешного
|
||||||
|
`nginx -t` и progress signal command перехватывается или подавляется; при
|
||||||
|
ошибке сохранённая диагностика полностью печатается в stderr. Любой stderr на
|
||||||
|
success path считается дефектом интеграции с Certbot.
|
||||||
Контролируются expiry days и последняя успешная попытка. Staging CA используется
|
Контролируются expiry days и последняя успешная попытка. Staging CA используется
|
||||||
в rehearsal, чтобы не исчерпать лимиты.
|
в rehearsal, чтобы не исчерпать лимиты.
|
||||||
|
|
||||||
@@ -219,6 +232,9 @@ Bitrix placement может требовать embedding: для exact `/bitrix/
|
|||||||
- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck;
|
- внутренний `GET /nginx-health/live` возвращает static 200 и доступен Docker healthcheck;
|
||||||
- внешний health публикуется только если нужен мониторингу, с allow-list;
|
- внешний health публикуется только если нужен мониторингу, с allow-list;
|
||||||
- nginx health не утверждает готовность upstream;
|
- nginx health не утверждает готовность upstream;
|
||||||
|
- Docker healthcheck использует только binary, гарантированно присутствующий
|
||||||
|
и проверенный внутри exact pinned nginx digest; `wget`/`curl` запрещены, если
|
||||||
|
их наличие не подтверждено image inventory;
|
||||||
- внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и callback route;
|
- внешняя synthetic проверка отдельно проверяет TLS, redirect, public API, auth discovery и callback route;
|
||||||
- upstream `/health/ready` не агрегируется публично без решения ops.
|
- 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.
|
`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
|
## 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.
|
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
|
### 15.2. Env и secrets Message Safety на ВМ2
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -855,7 +863,7 @@ Runtime secrets `MESSAGE_SAFETY_DATABASE_URL`, `MESSAGE_SAFETY_REDIS_URL`, `MESS
|
|||||||
|
|
||||||
### 15.3. Emergency mode
|
### 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
|
### 15.4. Performance acceptance MVP
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,10 @@ Legal retention/erasure имеет приоритет; изменение тре
|
|||||||
- без host ports;
|
- без host ports;
|
||||||
- config read-only, `otel-queue` volume rw;
|
- config read-only, `otel-queue` volume rw;
|
||||||
- non-root, read-only rootfs, tmpfs `/tmp`, drop capabilities, no-new-privileges;
|
- 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;
|
- initial limit: 0.5 CPU/512 MiB, queue disk 5–10 ГБ; уточнить load test;
|
||||||
- healthcheck extension;
|
- healthcheck extension;
|
||||||
- restart policy с backoff;
|
- 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.
|
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.
|
Доступ к Docker socket запрещён. Для container metrics используется безопасный exporter/hostmetrics, а не unrestricted socket mount.
|
||||||
|
|
||||||
## 15. Security
|
## 15. Security
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ free -h
|
|||||||
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
|
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
|
||||||
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
|
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
|
||||||
- [ ] UFW и DOCKER-USER активны после restart Docker.
|
- [ ] 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 закреплены поддерживаемой версией.
|
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
|
||||||
- [ ] NTP active; disk/swap соответствуют sizing.
|
- [ ] NTP active; disk/swap соответствуют sizing.
|
||||||
- [ ] Break-glass процедура сохранена вне VM.
|
- [ ] Break-glass процедура сохранена вне VM.
|
||||||
@@ -515,7 +520,10 @@ cd <BACKEND_ROOT>
|
|||||||
DOCKER_BUILDKIT=1 docker compose build --pull
|
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:
|
Frontend:
|
||||||
|
|
||||||
@@ -566,6 +574,10 @@ Volumes:
|
|||||||
- `otel-queue`;
|
- `otel-queue`;
|
||||||
- никаких PG data volumes.
|
- никаких PG data volumes.
|
||||||
|
|
||||||
|
Каждый `otel-queue` перед collector подготавливает idempotent
|
||||||
|
`otel-queue-init` (`10001:10001 0700`, без сети/secrets, только
|
||||||
|
`CHOWN/FOWNER`); collector стартует только после успешного one-shot.
|
||||||
|
|
||||||
Проверить:
|
Проверить:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -581,7 +593,11 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
|
|||||||
- [ ] Только nginx публикует ports.
|
- [ ] Только nginx публикует ports.
|
||||||
- [ ] Internal services не подключены к public без причины.
|
- [ ] Internal services не подключены к public без причины.
|
||||||
- [ ] Named volumes созданы и permissions проверены.
|
- [ ] Named volumes созданы и permissions проверены.
|
||||||
|
- [ ] OTEL ownership init завершился `0`, а write-test проходит от collector
|
||||||
|
UID; root collector/`0777` не используются.
|
||||||
- [ ] Container resource limits/healthchecks заданы.
|
- [ ] Container resource limits/healthchecks заданы.
|
||||||
|
- [ ] Healthcheck-команды выполнены в exact pinned digests; отсутствуют
|
||||||
|
`ExitCode 127` и зависимости от несуществующих `wget`/`curl`.
|
||||||
- [ ] `docker compose config --quiet` success.
|
- [ ] `docker compose config --quiet` success.
|
||||||
|
|
||||||
## 12. Stage 9 — TLS bootstrap, фаза 1
|
## 12. Stage 9 — TLS bootstrap, фаза 1
|
||||||
@@ -640,6 +656,9 @@ HSTS пока не включать. Проверить chain/hostname/redirect,
|
|||||||
5. записать структурированный результат и метрику времени до истечения;
|
5. записать структурированный результат и метрику времени до истечения;
|
||||||
6. вернуть ненулевой exit code при ошибке, чтобы сработал alert;
|
6. вернуть ненулевой exit code при ошибке, чтобы сработал alert;
|
||||||
7. не удалять действующий сертификат при неуспешном renew.
|
7. не удалять действующий сертификат при неуспешном renew.
|
||||||
|
8. на success path вернуть `0` с пустым stderr; benign output `nginx -t` и
|
||||||
|
signal command подавить/перенаправить, полную диагностику печатать только
|
||||||
|
при ошибке.
|
||||||
|
|
||||||
Пример unit `/etc/systemd/system/han-chat-cert-renew.service`:
|
Пример unit `/etc/systemd/system/han-chat-cert-renew.service`:
|
||||||
|
|
||||||
@@ -1278,6 +1297,8 @@ certbot delete active cert
|
|||||||
- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены;
|
- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены;
|
||||||
- `deploy` не имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units;
|
- `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;
|
- один 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;
|
- container hardening и сетевые границы соответствуют arch-06;
|
||||||
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
|
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
|
||||||
- Redis/Collector volumes/resources/security работают;
|
- 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.
|
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 запрещены.
|
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 минимум дважды в год.
|
При потере ВМ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