# arch-10. Контракт развёртывания > Канонический контракт rollout для всех application VM. OS-роли, SSH/sudo, secrets delivery и hardening — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md); этот документ их не ослабляет. > Процедуры конкретной машины — [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md) и [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md). > Nginx TLS — [`arch-08-nginx.md`](arch-08-nginx.md). Redis — [`arch-09-redis.md`](arch-09-redis.md). Telemetry — [`arch-07-observability.md`](arch-07-observability.md). Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). ## Назначение Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2** и то, что связывает их как систему: независимый deploy, VPC/SG, managed PG, S3, роли `deploy`/`admin`, секреты, TLS/ACME процедура, порядок cutover Safety, backup/rollback/DR. Команды сервисов конкретной машины и её Compose **не** дублируются здесь. Агент репозитория VM читает этот контракт плюс свой runbook. Прямые `docker compose` команды выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon. Значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. ## 1. Неподвижные правила 1. Один root Compose project описывается в `` своей VM; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`. 2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle. 3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access. 4. `/internal/*` не маршрутизируется публично. 5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP. 6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL. 7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты. 8. Миграции выполняются отдельными one-shot steps до новой версии приложения. 9. Production Message Safety работает только на ВМ2; local stub/fallback на ВМ1 запрещён. Caller ВМ1 использует private HTTPS, service token и проверку internal CA, при недоступности — fail closed. 10. `bitrix-sync` работает только на ВМ2 и вводится после preflight/cutover gates module-07; до подтверждённого cutover public webhook остаётся закрыт на edge. 11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо. 12. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files. 13. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06. ## 2. Роли и обозначения - **Cloud admin**: VPC, VM, PG, S3, DNS/security groups. - **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo. - **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя. - **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations. - **Bitrix admin**: local app, connector, Open Line 8, callbacks. - **Security owner**: secrets, Keycloak admin MFA, firewall, retention. - **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off. - **Rule Pack Owner**: rules bundle, corpus, monitor report и version release. - **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow. - **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal. ```text например chat.example.ru публичный IPv4 ВМ1 отдельный public host ВМ2, например processing.example.ru публичный IPv4/LB address ВМ2 приватный IPv4 VM например 10.20.0.0/24 private FQDN/IP managed PG 5433 для Selectel PgBouncer; иной порт — только фактическое значение другого provider/endpoint han_chat URL репозитория этой VM /opt/han-chat/backend immutable tag/git SHA адрес ops, не placeholder в реальном запуске разрешённый портал согласованное в Direct имя отправителя фактический статический egress IP `sms-worker` контролируемый номер для provider smoke ``` `` и `` **разные** у ВМ1 и ВМ2. ## 3. Stage 0 — решения до provisioning Зафиксировать: region/AZ и VPC; hostnames и TTL DNS; VM image Ubuntu 24.04 LTS; sizing каждой VM; PG plan/storage/backups/PITR; S3 region/endpoint/bucket names; container registry и immutable image tags/digests; remote observability backend; RPO/RTO и maintenance window; ответственных за alerts/Bitrix/Keycloak; Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover. Sizing и load gates конкретной машины — профильный runbook. Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts. ### Gate 0 - [ ] Владельцы и maintenance window назначены. - [ ] RPO/RTO приняты хотя бы временно: ориентир RPO PG ≤15 минут/PITR, RTO ≤4 часа. - [ ] Решено: images pull из registry или build на VM. - [ ] Remote telemetry backend выбран либо явно принят ограниченный debug-only режим. - [ ] Риск mock OTP до SMS cutover и Safety stub письменно принят; real SMS не включается без gates module-11. **Ожидаемый результат:** есть release checklist с конкретными values; не создано ни одной публичной БД/Redis. ## 4. Stage 1 — VPC, DNS и security groups Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion. | Source | Destination | Port | Rule | |---|---|---:|---| | trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `` | allow | | internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application | | internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook | | ВМ1 SG | ВМ2 | TCP 8443 | private TLS only | | ВМ1/ВМ2 service SG | managed PG | `` | allow по нужным DB roles | | ВМ2 collector | private SigNoz | TCP 4317 | allow | | ВМ2 workers | S3 endpoints | TCP 443 | allow | | ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow | | ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow | | ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow | | internet | managed PG | any | deny | | internet | ВМ2 | any кроме nginx 80/443 | deny ingress | | internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public | ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием. DNS: `A `, `A `, private DNS `processing.internal → `. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS. ### Gate 1 - [ ] PG не имеет public endpoint. - [ ] SSH доступен только trusted source. - [ ] Снаружи открыты только 80/443/ограниченный SSH. - [ ] DNS стабильно разрешается с нескольких resolver. - [ ] Каждая VM достигает private PG и своих внешних HTTPS endpoints. **Ожидаемый результат:** `nc -vz ` с VM успешен; с внешней машины PG недоступен. ## 5. Stage 2 — hardening Ubuntu Процедура первичная для **каждой** application VM и выполняется только по её профильному production runbook. Перед production: review версии; не передавать IP/ключи в git; проверить unattended upgrades; `deploy` не в группе `docker`; `AllowTcpForwarding no` по умолчанию; отдельный `tunnel` при необходимости; root-owned systemd-units и `/etc/sudoers.d/deploy` без wildcard. `PUBLIC_DOCKER_PORTS` задаёт профильный runbook (ВМ1: `80,443`; ВМ2 — свои public 80/443, private 8443 не internet). Проверки: `sshd -t`, UFW, fail2ban, Docker/Compose versions, `iptables -L HAN-CHAT-DOCKER`, timedatectl, disk/swap. Второй SSH session как `deploy`, затем отключить root/password login. ### Gate 2 - [ ] SSH key login `deploy` проверен во втором сеансе. - [ ] Root/password auth выключены. - [ ] `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. **Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service. ### 5.1. Lockdown private/no-egress VM Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06. Checklist закрытия public IP/SSH/egress и запрет постоянного open egress — arch-06; повторное открытие только break-glass с повтором lockdown. ## 6. Stage 3 — managed PostgreSQL До создания схем включить daily backup, PITR, encryption at rest, TLS `sslmode=verify-full` + provider CA, alerts (disk/connections/CPU/IO/replication/backup/CA), deletion protection. CA: `/opt/han-chat/secrets/pg/ca.pem`, `root:deploy` `0440` (либо `0400`). Режимы `disable`/`allow`/`prefer`/`require` без проверки CA для production запрещены. Публичные HTTPS-сертификаты `` и `` выпускаются отдельно через Let's Encrypt на Stage 9 nginx соответствующей VM, не в PostgreSQL. Целевая модель ролей: admin/bootstrap; migration role каждого schema с DDL; runtime без DDL. Прототип `init-managed-postgres.py` слишком широк для production runtime. Шесть schemas: `han_app`, `bitrix_local`, `bitrix_sync`, `keycloak`, `message_safety`, `sms`. Schema owner = migration role; runtime: `USAGE`, DML и sequence grants только на свои objects; `bitrix_sync_user` — только grants module-07 §13. Ownership миграций: 1. `api-backend` Alembic — `han_app`; 2. `bitrix-local-app` — `bitrix_local`; 3. `message-safety` stub не создаёт PG tables до production implementation; 4. `bitrix-sync` — schema `bitrix_sync`; api-backend отдельно мигрирует shared `han_app.sync_queue`; 5. Keycloak — standard tables; custom provider — свои migrations; 6. `sms-service` — schema `sms`; runtime `sms_user` без доступа к `han_app`/`keycloak`. Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается. ### Gate 3 - [ ] Backups/PITR/TLS/deletion protection включены. - [ ] Шесть schemas/roles созданы, включая `sms`/`sms_user`. - [ ] Runtime roles не имеют DDL/чужого доступа. - [ ] Migration credentials отделены от runtime. - [ ] Empty/previous-version migration test успешен. - [ ] PITR restore point создан перед первым release. ## 7. Stage 4 — S3 Три приватных bucket: `-quarantine`, `-attachments`, `-documents`. Public ACL/listing выключены. Versioning для data buckets по policy; SSE включить. IAM: API role — exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data; Safety role — **read-only quarantine**; backup/ops отдельно; frontend — никаких permanent credentials. Отдельный IAM principal ВМ2. CORS quarantine: `AllowedOrigins` только `https://`; PUT; headers `Content-Type`, `If-None-Match`, checksum; не `*` origin с credentials. Lifecycle: quarantine failed/orphan expire 48 ч только при отсутствии active `safety_tasks`; incomplete multipart abort 1 день; attachments/documents без auto-delete до legal retention. ### Gate 4 - [ ] Все buckets private. - [ ] API key не может list/write вне exact scope. - [ ] Safety key не может write/delete. - [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`. - [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version. - [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся. - [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call. - [ ] Quarantine lifecycle не удалит active `safety_tasks`. - [ ] Data lifecycle соответствует retention. ## 8. Общий layout релиза и секретов На каждой VM: ```text /opt/han-chat/ backend/ # checkout текущего release releases// # optional immutable release dirs secrets/ # не в git backups/ # только metadata/short-lived encrypted artifacts ``` Рекомендуемый rollout — immutable images из registry. Deploy из mutable branch без recorded SHA запрещён. `latest` отсутствует. `.env` — только несекретный config и `SECRETS_SOURCE=file|selectel`. Секреты выдаёт `deployment/secrets/han-secrets`. `docker compose config` может раскрыть secrets; stdout не публиковать. Validation: нет `change-me`, paired tokens equal, PG TLS, public HTTPS, `FRONTEND_DEV_PROXY_ENABLED=false` на production-like. Состав `.env`/secret groups — профильный runbook. ## 9. TLS/ACME процедура Канонические bootstrap, renewal, validation и safe reload задаёт [`arch-08-nginx.md`](arch-08-nginx.md). Rollout каждой VM применяет эту модель независимо, без `compose down` и без named volume сертификатов: - root-only ACME state: `/etc/letsencrypt`; - read-only для nginx host staging: `/var/lib/han-chat/public-tls`; - host ACME webroot: `/var/lib/han-chat/acme`; - root-owned systemd timer/hook дважды в сутки с `flock`; пользователь `deploy` может запускать только утверждённый unit и не получает доступ к ACME state; - staging CA rehearsal предшествует production issuance; после renewal root hook проверяет certificate/key, атомарно обновляет staging, выполняет container `nginx -t` и только затем HUP; - success path возвращает `0` с пустым stderr; ошибка сохраняет действующий certificate/config и поднимает alert (<21 дней, page <7 дней). Host ВМ1 — ``, ВМ2 — ``; private `8443` использует internal CA, не Let's Encrypt. Ни ACME state, ни staging между VM не разделяются. ## 10. Сквозной порядок startup и cutover 1. На ВМ2 unit поднимает Redis Safety и local Collector. 2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`. 3. Последним на ВМ2 — nginx public `80/443` и private `8443`. 4. На ВМ1 — Redis/Collector, API, SMS, Keycloak, local app, edge nginx. 5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1. Legacy single-VM `docker compose up` не является evidence готовности target ВМ2. Один `message_id` нельзя одновременно отправлять в v1 и v2. После cutover ВМ1 не содержит local Safety/Redis DB2. При потере ВМ2 fail-open запрещён. Reprovision ВМ2 из immutable image/config; Redis пустой; gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год. ## 11. Observability, opening traffic, backup, rollback Gate 5–14 не дублируются в этом общем контракте: это профильные release gates конкретной VM, описанные в `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md` и executable runbook соответствующего репозитория. Нумерация внутри VM2 runbook локальна его технической приёмке; системное открытие трафика всегда требует подписанных результатов обоих контуров. **Gate 15 — observability:** arch-07 и профильный module-09 VM spec; сквозной `request_id` через nginx ВМ1 → API → Safety ВМ2 обязателен до объявления production-ready. **Gate 16 — opening traffic:** снять maintenance, включить HSTS после TLS test, сохранить digests/revisions, restore point и on-call, затем наблюдать 60 минут. До открытия должны быть подписаны Gate 0–4 этого документа, применимые Gate 5–14 профильных runbook обеих VM и Gate 15. Backup: PG daily+PITR — business restore; S3 versioning; Redis не business backup; Keycloak в PG. Restore rehearsal в isolated VPC без production DNS/Bitrix callbacks. Rollback приложения: previous immutable digests, без Alembic downgrade, `SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true`. Backward-incompatible migration — только forward-fix или PITR. TLS: оставить старый config/cert при failure. Запрещены: `docker compose down -v`, `docker system prune -a --volumes`, `DROP DATABASE` / `DROP SCHEMA`, recursive S3 delete, `certbot delete` active cert, unbounded logs, Redis `KEYS/FLUSH*`, ad-hoc DB DELETE. Routine (автомат ежедневно): health/synthetics, PG backup/PITR, TLS expiry, disk/OTEL queue, Redis AOF/memory, DLQ/quarantine age. Еженедельно: image CVE, login/rate-limit trend, S3 inventory. Ежемесячно: OS patch, secret review, capacity, runbook drill. ```bash cd docker compose ps docker compose logs --since=15m docker stats --no-stream docker system df docker compose exec -T nginx nginx -t ``` Incident triage: ```bash cd date -Is docker compose ps docker stats --no-stream docker compose logs --since=10m --tail=500 df -h free -h sudo ss -lntp sudo iptables -L HAN-CHAT-DOCKER -n -v ``` PG: readonly `select now(), count(*) from pg_stat_activity`. Redis — только ops ACL `PING`. Secret literal не вставлять в ticket. Не replay message/DLQ до idempotency и ambiguous Bitrix outcome. Типовые сценарии: API 503 — DB/Redis/JWKS/Safety circuits; send timeout — checkpoint, не новый key; Bitrix down — OAuth/circuit/DLQ; Redis loss — clean restart + polling; PG outage — не restart storm; disk full — known cache/old images после inventory; cert expiry — webroot/staging; secret leak — rotate, telemetry deletion. Upgrades: notes → compatibility → PITR → staging → expand migration → one service at a time → E2E → contract later. Keycloak не пропускать unsupported majors. DR потеря VM: новая Ubuntu в VPC, hardening, DNS, secrets из vault не со старого disk, exact images, Redis можно clean, existing PG/S3, TLS, ordered startup. Потеря PG: PITR в new instance, остановить writes, reconcile S3 orphans. Потеря S3: без versioning полное восстановление невозможно; отключить file ops. Compromise: isolate, forensic snapshot, rotate all, clean deploy, notify. Перед teardown: inventory без secrets, revoke Bitrix/credentials, legal hold, DNS drain, deletion protection — отдельное approval, shared VPC/PG/S3 проверить. ## 12. Общий Definition of Done - VM/VPC/DNS/SG/hardening соответствуют Gate 1–2; - managed PG private/TLS/backups/least privilege/migrations работают; - S3 private/IAM/CORS/lifecycle проверены; - `deploy` не имеет Docker/root-equivalent доступа; - независимые public 80/443; ВМ2 дополнительно private 8443; - после Safety cutover ВМ1 вызывает ВМ2 только по private HTTPS с проверенным CA bind; - container hardening — arch-06; - для каждой private/no-egress VM завершён lockdown; - backup restore и rollback rehearsed; - ops/incident/upgrade/DR owners назначены; - все assumptions/TBD приняты до открытия traffic. Профильный DoD VM дополняет свои сервисы, Compose и cutover steps. ## 13. Допущения, TBD и конфликты **Допущения:** D-A1 отдельные public hosts; D-A2 обязательный минимум — local Collector; D-A3 managed PG private/TLS/PITR; D-A4 Bitrix portal/connector/line — значения architecture; D-A5 mock OTP до SMS cutover как documented risk. **TBD:** D-TBD1 domains/Expo redirect/Bitrix frame ancestors; D-TBD2 final VM1/PG sizing, public SLO, RPO/RTO; D-TBD3 legal retention; D-TBD4 secret manager; D-TBD5 Safety v2 cutover; D-TBD6 bitrix-sync cutover; D-TBD7 pinned versions; D-TBD8 exact CLI после реализации; D-TBD9 Keycloak admin VPN/MFA; D-TBD10 CSP/CORS/IAM; D-TBD11 SigNoz auth/retention/alert route. **Конфликты:** module-07 documentation ≠ cutover; v1 stub ≠ production Safety; prototype PG init слишком широк; prototype TLS downtime; prototype `/bitrix-internal/*` запрещён; Compose/env могут ещё не содержать VM2/SMS artifacts. ## 14. Ссылки - ВМ1: [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md). - ВМ2: [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md). - Исполняемые процедуры: [`RUNBOOK.production.ru.md`](../VM1_app/codebase/backend/deployment/RUNBOOK.production.ru.md) для ВМ1 и [`RUNBOOK.ru.md`](../VM2_services/codebase/services/deployment/RUNBOOK.ru.md) для ВМ2.