Files
han-app/modules/module-10-deployment-runbook.md
T

79 KiB
Raw Blame History

module-10. Runbook развёртывания HAN Chat

Статус: целевой runbook ВМ1/ВМ2. Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy.
Все значения в <УГЛОВЫХ_СКОБКАХ> — placeholders. Команды с cd <BACKEND_ROOT> требуют подстановки реального пути корня backend-репозитория на VM.
Канонические источники: ../architectory/README.md, ../architectory/arch-00-glossary.md, ../architectory/arch-01-system-architecture.md, ../architectory/arch-02-api-contracts.md, ../architectory/arch-03-docker-compose-blueprint.md, ../architectory/arch-04-settings-and-content.md, ../architectory/arch-05-agent-development-process.md, ../architectory/arch-06-service-hosting-security.md, module-01-api-backend.mdmodule-09-observability.md.

1. Неподвижные правила

  1. Один root Compose project описывается в <BACKEND_ROOT>; в 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. Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
  10. bitrix-sync вводится только после выполнения preflight/cutover gates module-07; до этого BITRIX_SYNC_ENABLED=false, 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.

Прямые docker compose команды в этом runbook выполняются admin только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать deploy доступ к Docker daemon.

2. Роли и обозначения

  • Cloud admin: VPC, VM, PG, S3, DNS/security groups.
  • Deploy operator / OS user deploy: запуск утверждённых release/rollback/migration systemd-units и root-owned Message Safety mode helper; без группы 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.

Placeholders:

<PUBLIC_HOST>          например chat.example.ru
<VM_PUBLIC_IP>         публичный IPv4 VM
<PROCESSING_PUBLIC_HOST> отдельный public host ВМ2, например processing.example.ru
<VM2_PUBLIC_IP>        публичный IPv4/LB address ВМ2
<VM_PRIVATE_IP>        приватный IPv4 VM
<VPC_CIDR>             например 10.20.0.0/24
<PG_PRIVATE_HOST>      private FQDN/IP managed PG
<PG_PORT>              5432 или 6432
<PG_DATABASE>          han_chat
<BACKEND_REPO_URL>     URL репозитория
<BACKEND_ROOT>         /opt/han-chat/backend
<RELEASE>              immutable tag/git SHA
<ACME_EMAIL>           адрес ops, не placeholder в реальном запуске
<BITRIX_PORTAL>        разрешённый портал
<IDGTL_SENDER_NAME>    согласованное в Direct имя отправителя
<IDGTL_STATIC_EGRESS_IP> фактический статический egress IP `sms-worker`
<IDGTL_TEST_PHONE>     контролируемый номер для provider smoke

3. Stage 0 — решения до provisioning

3.1. Зафиксировать параметры

  • region/availability zone и VPC;
  • hostnames и TTL DNS;
  • VM image Ubuntu 24.04 LTS;
  • sizing;
  • 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 ВМ2 без local Grafana stack:

  • VM: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap;
  • managed PG: минимум 2 vCPU, 4 ГБ RAM, 50 ГБ, HA по возможности;
  • Redis limit: 512 МиБ;
  • OTEL Collector: 512 МиБ + 510 ГБ queue;
  • свободный диск VM после pull/build: не менее 30%.

Это baseline, не гарантия. До real traffic обязателен load test module-05 §15.4:

  • sustained 10 text checks/s: p95 ≤2 с, p99 ≤5 с;
  • sustained 2 file checks/s на 5 worker slots: среднее processing ≤2.5 с, p95 ≤60 с, public wait ≤300 с;
  • 100 pending принимаются; 101-й file POST получает retryable 503 без новой task;
  • RPS overflow даёт 429 + Retry-After;
  • long Safety poll не блокирует WS/read API;
  • если gate не пройден, увеличить slots/CPU/clamd scan lanes и повторить; production traffic не открывать.

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, VM, DNS и security groups

4.1. Сеть

Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion.

Security groups:

Source Destination Port Rule
trusted ops CIDR/VPN ВМ1, ВМ2 SSH <SSH_PORT> 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 <PG_PORT> 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 состоянием.

4.2. DNS

Создать A <PUBLIC_HOST> → <VM1_PUBLIC_IP>, A <PROCESSING_PUBLIC_HOST> → <VM2_PUBLIC_IP> и private DNS processing.internal → <VM2_PRIVATE_IP>. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS.

Проверка с рабочей станции:

dig +short <PUBLIC_HOST>
dig +short <PROCESSING_PUBLIC_HOST>

Ответ должен совпасть с <VM_PUBLIC_IP>.

Gate 1

  • PG не имеет public endpoint.
  • SSH доступен только trusted source.
  • Снаружи открыты только 80/443/ограниченный SSH.
  • DNS стабильно разрешается с нескольких resolver.
  • VM достигает private PG и внешних HTTPS endpoints.

Ожидаемый результат: nc -vz <PG_PRIVATE_HOST> <PG_PORT> с VM успешен; с внешней машины PG недоступен.

5. Stage 2 — hardening Ubuntu и deploy user

5.1. Первичный вход

Войти cloud user, добавить отдельный deploy key. Не отключать пароль/root до проверки второго SSH-сеанса.

Прототипный скрипт можно адаптировать:

sudo DEPLOY_USER=deploy \
  DEPLOY_DIR=/opt/han-chat \
  SSH_PORT=<SSH_PORT> \
  SWAP_SIZE_GB=4 \
  PUBLIC_DOCKER_PORTS=80,443 \
  ./deploy/setup-vm-han-chat.sh

Скрипт из ../../HAN_chat/deploy/setup-vm-han-chat.sh полезен для UFW, fail2ban, Docker и DOCKER-USER, но перед production:

  • проверить его версию/review;
  • не передавать реальные IP/ключи в git;
  • проверить auto reboot unattended upgrades относительно maintenance;
  • гарантировать, что deploy не добавлен в группу docker (это root-equivalent);
  • оставить AllowTcpForwarding no по умолчанию; если нужен DB tunnel, создать отдельного tunnel с AllowTcpForwarding local, конкретным PermitOpen, без sudo/TTY/agent/X11 forwarding;
  • установить root-owned systemd-units и /etc/sudoers.d/deploy с полными командами и конкретными unit names без wildcard.

5.2. Проверки

sudo sshd -t
sudo ufw status verbose
sudo fail2ban-client status sshd
docker version
docker compose version
sudo iptables -L HAN-CHAT-DOCKER -n -v
timedatectl status
df -h
free -h

Открыть второй SSH session как deploy, затем отключить root/password login. Production .env позже содержит только non-secret config; runtime secrets доставляются по arch-06.

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.
  • Docker Engine/Compose plugin закреплены поддерживаемой версией.
  • NTP active; disk/swap соответствуют sizing.
  • Break-glass процедура сохранена вне VM.

Ожидаемый результат: reboot VM не теряет SSH, firewall и Docker service.

5.3. Дополнительный lockdown private/no-egress VM

Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06.

До закрытия временного доступа:

  • SSH разрешён только из trusted ops CIDR и только по ключам.
  • Пакеты/images получены из утверждённых источников, версии/digests зафиксированы.
  • Health/readiness успешны.
  • Проверены необходимые private flows (например app-VM → OTLP/SigNoz).
  • Проверен private путь администрирования через bastion/VPN.
  • Секреты размещены root-owned файлами; временные копии удалены.

Lockdown:

  • Public IP удалён, если поддерживается и не нужен.
  • Public SSH и любой internet ingress удалены из cloud SG и host firewall.
  • Общий internet egress закрыт; оставлены только явно утверждённые private flows.
  • С внешней машины SSH и service ports недоступны.
  • С VM не проходит неразрешённый internet egress.
  • Из private network работают SSH и обязательные service flows.
  • Временные bootstrap credentials/rules/files удалены.
  • Результат и время закрытия записаны в release checklist.

Повторное открытие ingress/egress выполняется только как ограниченная по CIDR/destination и времени break-glass операция. После неё весь lockdown checklist повторяется.

6. Stage 3 — managed PostgreSQL

6.1. Защита и восстановление managed PostgreSQL

До создания схем включить и проверить настройки managed PostgreSQL:

  • Daily backup — автоматический полный снимок БД не реже одного раза в сутки. Нужно задать срок хранения, например 7–14 дней, и убедиться, что резервные копии размещаются отдельно от вычислительного узла БД. Наличие backup без периодической проверки восстановления не считается достаточным.
  • PITR (Point-in-Time Recovery) — восстановление состояния БД на выбранный момент времени между полными backup за счёт архивации WAL. Это позволяет откатиться, например, к состоянию непосредственно перед ошибочной миграцией или удалением данных. Период доступного восстановления должен соответствовать принятому RPO.
  • Encryption at rest — шифрование дисков, backup и WAL на стороне провайдера managed PostgreSQL. Ключ управляется провайдером либо KMS организации; пароль пользователя БД не заменяет это шифрование.
  • TLS для соединения с PostgreSQL — шифрование трафика между контейнерами на VM и managed PostgreSQL с обязательной проверкой имени сервера. CA-сертификат обычно выдаёт провайдер PostgreSQL. Это не публичный сертификат сайта и не сертификат Let's Encrypt.
  • Alerts — уведомления как минимум о нехватке диска, исчерпании подключений, высокой загрузке CPU/IO, replication lag (если есть реплики), неуспешном backup и истечении/замене CA.
  • Deletion protection — запрет удаления кластера обычной командой/API. Отключение защиты должно быть отдельным подтверждаемым действием администратора. Эта настройка не защищает от DROP TABLE, поэтому least privilege и backup всё равно обязательны.

CA managed PostgreSQL скачать из панели или документации провайдера в защищённый путь VM, например /opt/han-chat/secrets/pg/ca.pem, с владельцем root:deploy и mode 0440 (либо 0400, если файл читает один пользователь). Все DSN используют sslmode=verify-full и sslrootcert=/run/secrets/pg-ca.pem либо эквивалент драйвера. Режимы disable, allow, prefer и require без проверки CA для production запрещены.

Публичные HTTPS-сертификаты <PUBLIC_HOST> и <PROCESSING_PUBLIC_HOST> выпускаются отдельно через Let's Encrypt на Stage 9 и устанавливаются только в nginx соответствующей VM, а не в PostgreSQL.

6.2. Роли

Целевая модель разделяет:

  • admin/bootstrap role;
  • migration role каждого schema с DDL;
  • runtime role без DDL.

Прототип init-managed-postgres.py выдаёт runtime roles CREATE на schema и печатает DSN. Это допустимо только для bootstrap/dev, но слишком широко для production runtime. Перед production адаптировать:

  1. создать шесть schemas: han_app, bitrix_local, bitrix_sync, keycloak, message_safety, sms;
  2. создать runtime roles;
  3. создать migration roles либо controlled admin job;
  4. schema owner = migration role;
  5. runtime: USAGE, DML и sequence grants только на свои objects;
  6. ALTER DEFAULT PRIVILEGES от migration owner;
  7. запретить чужие schemas и public schema create;
  8. bitrix_sync_user получает только column/table grants и approved procedures из module-07 §13 после применения полной sync migration; broad schema write запрещён.

Команда bootstrap требует <PROJECT_PATH>:

cd <BACKEND_ROOT>
cp deploy/pg-init.env.example deploy/pg-init.env
chmod 600 deploy/pg-init.env
# заполнить private host/database/admin и generated passwords
set -a; source deploy/pg-init.env; set +a
python3 deploy/init-managed-postgres.py
unset HAN_PG_ADMIN_PASSWORD

Не сохранять stdout с DSN в shared logs. Исторический init-managed-postgres.sql содержит placeholder passwords и database postgres; для целевой БД применять только после review и замены database name.

6.3. Проверка least privilege

Для каждого runtime user:

psql "host=<PG_PRIVATE_HOST> port=<PG_PORT> dbname=<PG_DATABASE> user=<RUNTIME_USER> sslmode=verify-full sslrootcert=<PG_CA_PATH>" \
  -c "select current_user, current_setting('search_path');"

Негативно проверить CREATE TABLE и доступ к чужой schema — они должны завершиться permission denied.

6.4. Migration policy

Порядок ownership:

  1. api-backend Alembic владеет han_app, triggers, seed;
  2. bitrix-local-app Alembic владеет bitrix_local;
  3. message-safety stub не создаёт PG tables до production implementation;
  4. bitrix-sync migration role владеет schema bitrix_sync; api-backend Alembic отдельно мигрирует shared han_app.sync_queue, triggers и mapping;
  5. Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations;
  6. sms-service владеет versioned migrations/seed schema sms; runtime sms_user не имеет доступа к han_app/keycloak.

Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается; rollback приложения требует backward-compatible schema.

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.

Ожидаемый результат: runtime SELECT 1 успешен, unauthorized schema read/create запрещены.

7. Stage 4 — S3 buckets, IAM, CORS и lifecycle

Создать три приватных bucket:

  • <PREFIX>-quarantine;
  • <PREFIX>-attachments;
  • <PREFIX>-documents.

Public ACL/listing выключены. Versioning включить для data buckets по policy; server-side encryption включить.

IAM:

  • API role/key: exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data;
  • Safety role/key: read-only quarantine;
  • backup/ops role: отдельно;
  • frontend: никаких permanent credentials.

CORS quarantine:

[
  {
    "AllowedOrigins": ["https://<PUBLIC_HOST>"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type", "If-None-Match", "x-amz-checksum-sha256", "x-amz-*"],
    "ExposeHeaders": ["ETag", "x-amz-checksum-sha256"],
    "MaxAgeSeconds": 600
  }
]

Required signed headers для target flow: Content-Type, If-None-Match: *, checksum. Не разрешать * origin с credentials.

Lifecycle:

  • quarantine: failed/orphan expire через 48 ч и только при отсутствии active safety_tasks;
  • incomplete multipart upload: abort через 1 день;
  • attachments/documents: без auto-delete до legal retention;
  • noncurrent versions: policy после legal review.

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.

Ожидаемый результат: anonymous GET/PUT получает deny; API capability test проходит.

8. Stage 5 — repository и release layout

На VM:

/opt/han-chat/
  backend/             # checkout текущего release
  releases/<RELEASE>/  # optional immutable release dirs
  secrets/             # не в git
  backups/             # только metadata/short-lived encrypted artifacts

Рекомендуемый rollout — immutable images из registry. Build на VM допустим для MVP, но требует reproducible Dockerfiles и достаточно диска.

sudo install -d -m 0755 -o deploy -g deploy /opt/han-chat
git clone <BACKEND_REPO_URL> <BACKEND_ROOT>
cd <BACKEND_ROOT>
git fetch --tags
git checkout --detach <RELEASE>
git status --short

Ожидается clean tree. Запретить deploy из mutable branch без recorded SHA.

Проверить структуру: root docker-compose.yml, service directories, nginx, keycloak, redis, observability, frontend artifact.

Gate 5

  • Checkout exact SHA/tag.
  • Working tree clean.
  • Images/Dockerfiles pinned, latest отсутствует.
  • SBOM/vulnerability scan без unresolved critical/high.
  • Root Compose — единственный production entrypoint.

9. Stage 6 — несекретный .env и runtime secrets

9.1. Создание

cd <BACKEND_ROOT>
umask 077
cp .env.example .env
chmod 600 .env

.env содержит только несекретный config и SECRETS_SOURCE=file|selectel. Пароли, токены, access keys, credential-bearing DSN и *_FILE пути в нём запрещены. Секреты выдаёт единый интерфейс:

deployment/secrets/han-secrets run --config .env -- <command>

Launcher устанавливает HAN_SECRETS_ACTIVE=1, не выводит значения и может передать paths-only manifest через HAN_RUNTIME_SECRET_MANIFEST. Файлы manifest должны быть абсолютными, недоступными group/other. Генерацию выполняет secret manager; не выполнять export SECRET=... и не вставлять значения в history.

9.2. Обязательные группы

  • APP_ENV, release/version, log level;
  • private PG host/port/database и TLS CA в config; runtime DSN в secret backend;
  • Redis ACL credentials/URLs DB0/1/2 только в secret backend;
  • public web/API/auth URLs;
  • Keycloak realm/audience/hostname/bootstrap/provider technical secrets;
  • SMS DB URL, парные Keycloak↔SMS tokens, Direct TOKEN_1, callback URL и отдельные callback credentials;
  • paired service tokens из arch-02;
  • Bitrix client/application/webhook/encryption secrets;
  • S3 endpoint/buckets/API and read-only Safety credentials;
  • OTEL endpoint/remote exporter secrets;
  • nginx/TLS/rate limits;
  • frontend public build values.

Пары должны совпасть:

BITRIX_LOCAL_APP_INTERNAL_TOKEN == BITRIX_INTERNAL_API_TOKEN
BITRIX_API_FORWARD_TOKEN == BITRIX_API_INBOX_TOKEN
KEYCLOAK_SMS_SERVICE_TOKEN == SMS_SERVICE_TOKEN

Service token и webhook token — разные secrets.

9.3. Validation

Запустить scripts/validate-env в двух независимых режимах:

  • config: mandatory not empty, SECRETS_SOURCE, запрет secret keys/DSN credentials;
  • runtime: manifest/files или child environment без печати значений;
  • нет change-me, example IP/domain, default OTP;
  • URLs have correct schemes;
  • public URLs HTTPS, internal URLs service DNS;
  • PG TLS enabled;
  • paired tokens equal;
  • CORS/origins exact;
  • no duplicate keys;
  • FRONTEND_DEV_PROXY_ENABLED=false;
  • Safety timeout согласован с nginx;
  • secrets minimum length;
  • mock OTP risk flag explicitly accepted.
  • placeholders change-me/<...> запрещены; real mode требует sender/template/API key/callback credentials и recorded static egress IP;
cd <BACKEND_ROOT>
./scripts/validate-env .env
sudo systemctl restart han-secrets@production.service
sudo ./scripts/validate-env .env \
  --runtime-manifest /run/han-chat/secrets/manifest
sudo deployment/secrets/han-compose config --quiet

docker compose config может раскрыть resolved secrets; не сохранять/публиковать его stdout.

Gate 6

  • .env отсутствует в git и содержит только несекретный config.
  • Secret launcher и permissions manifest/files проверены.
  • Все placeholder/default secrets отклонены.
  • Paired tokens совпадают.
  • DSN private/TLS; URLs/issuer согласованы.
  • Validation и Compose interpolation успешны.
  • Secret recovery/rotation owner назначен.

10. Stage 7 — images и frontend static

Pull-вариант

cd <BACKEND_ROOT>
docker login <REGISTRY>
docker compose pull
docker image ls --digests

Registry token read-only и короткоживущий.

Build-вариант

cd <BACKEND_ROOT>
DOCKER_BUILDKIT=1 docker compose build --pull

Build не получает production secrets. Записать image digests.

Frontend:

cd <FRONTEND_PROJECT_PATH>
npm ci
npm run test
npx expo export --platform web

Скопировать artifact в versioned frontend-static volume/image. index.html revalidate, hashed assets immutable. Build env содержит только public URL/realm/client id. Проверить отсутствие service tokens/mock OTP/S3 keys командой secret scanner.

Gate 7

  • Все images доступны по digest.
  • Frontend build/tests успешны.
  • Static artifact не содержит secrets/source maps по policy.
  • nginx image/config содержит request-id module и TLS features.
  • Disk после pull/build >30% free.

11. Stage 8 — root Compose, networks и volumes

До запуска:

cd <BACKEND_ROOT>
docker compose config --services

В target release:

  • root Compose ВМ1: edge nginx, api-backend, keycloak, sms-service, sms-worker, bitrix-local-app, Redis DB0/DB1, local otel-collector;
  • root Compose ВМ2: собственный public/private nginx, message-safety-api, message-safety-worker, clamd, freshclam, bitrix-sync, Redis Safety, local otel-collector;
  • API и worker Safety используют один immutable image, но отдельные processes/containers. MVP: 1 API + 1 worker container с 5 file-worker slots; при провале gates сначала увеличиваются slots/worker replicas по queue depth.

Networks:

  • public: nginx и минимально Keycloak/frontend path;
  • backend: internal services/Redis;
  • egress: только утверждённые outbound workers; Keycloak в неё не входит, sms-worker входит;
  • observability: services + Collector.

Volumes:

  • redis-data;
  • ACME certs/webroot;
  • frontend static;
  • otel-queue;
  • никаких PG data volumes.

Проверить:

docker compose config | rg 'ports:|expose:|networks:|volumes:'

Если rg на VM нет, использовать reviewed script, не ручной визуальный просмотр. Единственные published mappings — nginx 80/443.

Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config read-only, queue bounded, no public OTLP.

Gate 8

  • Только nginx публикует ports.
  • Internal services не подключены к public без причины.
  • Named volumes созданы и permissions проверены.
  • Container resource limits/healthchecks заданы.
  • docker compose config --quiet success.

12. Stage 9 — TLS bootstrap, фаза 1

Прототип ssl-issue.sh останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен webroot two-phase, чтобы не делать compose down. Процедура выполняется независимо в root Compose каждой VM: для ВМ1 с <PUBLIC_HOST>, для ВМ2 с <PROCESSING_PUBLIC_HOST>.

Phase A: HTTP bootstrap

  1. DNS уже указывает на VM.
  2. Запустить nginx с bootstrap config: только /.well-known/acme-challenge/ и redirect; TLS block не требует отсутствующий cert.
  3. Запустить Certbot profile:

Сначала проверить процесс через Let's Encrypt staging CA, добавив --staging. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным --cert-name текущего host. Ни сертификат, ни ACME volume между VM не разделяются.

Production-выпуск:

cd <BACKEND_ROOT>
docker compose --profile tls-bootstrap up -d nginx
docker compose --profile certbot run --rm certbot certonly \
  --webroot -w /var/www/certbot \
  -d <PUBLIC_HOST> \
  --cert-name <PUBLIC_HOST> \
  --email <ACME_EMAIL> \
  --agree-tos --no-eff-email --non-interactive

Каталоги /etc/letsencrypt и /var/www/certbot должны быть общими named volumes для контейнеров certbot и nginx. Сертификат и закрытый ключ не копируются в репозиторий или image. TLS-конфигурация nginx использует:

/etc/letsencrypt/live/<PUBLIC_HOST>/fullchain.pem
/etc/letsencrypt/live/<PUBLIC_HOST>/privkey.pem

Phase B: TLS activation

cd <BACKEND_ROOT>
docker compose exec -T nginx nginx -t -c /tmp/nginx.conf
# активировать rendered TLS config атомарно
docker compose kill -s HUP nginx

HSTS пока не включать. Проверить chain/hostname/redirect, затем включить HSTS без preload.

Автоматическое обновление Let's Encrypt

Сертификаты Let's Encrypt действуют 90 дней. Проверку обновления выполнять дважды в сутки; Certbot сам обновляет сертификат только при приближении срока истечения. Для VM предпочтителен systemd timer, который вызывает репозиторный скрипт root Compose.

Скрипт <BACKEND_ROOT>/deploy/ssl-renew.sh должен:

  1. взять flock, чтобы исключить параллельные запуски;
  2. выполнить docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet;
  3. при успешном обновлении проверить docker compose exec -T nginx nginx -t -c /tmp/nginx.conf;
  4. только после успешной проверки выполнить docker compose kill -s HUP nginx;
  5. записать структурированный результат и метрику времени до истечения;
  6. вернуть ненулевой exit code при ошибке, чтобы сработал alert;
  7. не удалять действующий сертификат при неуспешном renew.

Пример unit /etc/systemd/system/han-chat-cert-renew.service:

[Unit]
Description=Renew HAN Chat Let's Encrypt certificate
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=oneshot
User=deploy
WorkingDirectory=<BACKEND_ROOT>
ExecStart=<BACKEND_ROOT>/deploy/ssl-renew.sh

Пример timer /etc/systemd/system/han-chat-cert-renew.timer:

[Unit]
Description=Check HAN Chat certificate renewal twice daily

[Timer]
OnCalendar=*-*-* 03,15:20:00
RandomizedDelaySec=30m
Persistent=true
Unit=han-chat-cert-renew.service

[Install]
WantedBy=timers.target

Установка и обязательная проверка расписания:

sudo systemctl daemon-reload
sudo systemctl enable --now han-chat-cert-renew.timer
sudo systemctl list-timers han-chat-cert-renew.timer
cd <BACKEND_ROOT>
docker compose --profile certbot run --rm certbot renew --dry-run
sudo systemctl start han-chat-cert-renew.service
sudo systemctl status han-chat-cert-renew.service --no-pager

Дополнительно настроить alert при остатке менее 21 дня и критический alert менее 7 дней. Проверять срок можно synthetic probe снаружи и метрикой exporter/скрипта на VM. Ошибка одного запуска renew не должна останавливать nginx.

Прототипные ssl-common.sh, ssl-renew.sh полезны концептуально, но должны работать с root Compose, не service compose.

Gate 9

  • Staging issuance rehearsal успешен.
  • Production cert chain/hostname valid.
  • HTTP только ACME + 308.
  • TLS 1.0/1.1 rejected; 1.2/1.3 accepted.
  • Renewal dry-run и safe reload успешны.
  • han-chat-cert-renew.timer включён, имеет следующий запуск и переживает reboot.
  • Alert expiry настроен.

13. Stage 10 — миграции и seed

Остановить public traffic либо использовать maintenance page до gate.

13.1. Preflight

cd <BACKEND_ROOT>
docker compose run --rm api-backend alembic current
docker compose run --rm bitrix-local-app alembic current

Создать PITR marker. Выполнить dry-run/SQL review в clone/staging.

13.2. Upgrade

cd <BACKEND_ROOT>
docker compose run --rm api-backend alembic upgrade head
docker compose run --rm bitrix-local-app alembic upgrade head

Для message-safety stub PG migration отсутствует. Перед full sync cutover сначала применяется backward-compatible api-backend migration shared queue/mapping/trigger, затем migration schema bitrix_sync; grants выдаются после обеих migrations и negative permission test. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.

13.3. Seed app_settings

Seed обязан быть idempotent/versioned и содержать все ключи arch-04. Выполнить migration или:

cd <BACKEND_ROOT>
docker compose run --rm api-backend python -m app.cli.seed_settings --file <PROJECT_PATH>/deploy/app-settings.production-like.yaml
docker compose run --rm api-backend python -m app.cli.validate_settings

Команды являются целевым интерфейсом; если CLI ещё не реализован, gate не проходить ручными ad-hoc INSERT без reviewed SQL.

Проверить public keys/DTO, consent versions/URLs, CORS host, file MIME/size, UX idle timeout. Не копировать phone/URLs из prototype без product approval.

Gate 10

  • PITR marker до migrations.
  • Expected Alembic revisions active.
  • Runtime users не выполняли DDL.
  • Seed idempotency проверена повторным запуском.
  • Mandatory settings valid; secrets отсутствуют в app_settings.
  • Backward compatibility с текущими images подтверждена.

13.4. Controlled rollout real SMS

До переключения Keycloak:

  1. применить App DB seed otp.phone.code_length, otp.phone.ttl_seconds, otp.phone.sms_order_timeout_ms;
  2. создать schema/role sms, применить migrations и idempotent seed sms_setting/active approved auth_otp;
  3. в test environment развернуть sms-service/worker с локальным mock Direct и выполнить contract/E2E;
  4. получить production Direct TOKEN_1, согласованные sender и template, отдельные callback credentials;
  5. определить egress IP фактическим запросом из sms-worker, подтвердить его статичность/NAT, записать в inventory и передать Direct для allowlist;
  6. развернуть production sms-service/worker и callback route, оставив KEYCLOAK_OTP_MOCK_ENABLED=true;
  7. применить Keycloak expand migration/SPI, мигрировать старые challenges по module-11;
  8. выполнить provider smoke отдельной ops-командой на <IDGTL_TEST_PHONE>; проверить journal, callback, redaction и отсутствие duplicate;
  9. только после подписанных evidence переключить KEYCLOAK_OTP_MOCK_ENABLED=false;
  10. проверить durable order до Direct response, resend/superseded, expiry snapshot, limits и verify при provider reject/timeout.

Production cutover запрещён при любом placeholder, несогласованном sender/template, отсутствующем API key/callback credentials, неподтверждённом callback IP или нестатическом egress IP. Direct API key — готовый TOKEN_1 для Basic, повторно Base64 не кодируется.

Rollback SMS: немедленно вернуть Keycloak в mock mode; не удалять schema/journal и не откатывать migrations без доказанной backward compatibility. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/uncertain; предпочтителен forward-fix.

13.5. Controlled rollout bitrix-sync

До включения BITRIX_SYNC_ENABLED=true:

  1. создать/проверить custom Contact fields user_id, registration flag, citizenship и занести их non-secret names в env; для universal CRM имя UF_CRM_<digits> преобразуется в ufCrm_<digits>, active filter использует 1, а wire-значения add/update подтверждаются contract test;
  2. создать smart process «Конфликты синхронизации», поля/стадии/ответственного/SLA и активировать validated bitrix_sync.settings;
  3. завести отдельный входящий webhook технического пользователя с минимальными правами module-07 §13;
  4. настроить два HTTP-webhook робота на Contact/alert receiver URLs https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/...?token=<receiver-token>; использовать отдельные высокоэнтропийные query tokens из штатного secret manager, исключить query/body из журналов; local app/event handler для CRM sync не создавать;
  5. применить expand migrations han_app, затем bitrix_sync, после чего выдать точечные GRANT и выполнить negative permission tests;
  6. зафиксировать cutover_watermark и one-shot операцией отменить существующие до него pending/retry contact-задачи с причиной initial_full_sync_cutover;
  7. не создавать backfill: это утверждённое ограничение первого релиза;
  8. запустить image с sync disabled, проверить /health/live, expected sync_disabled, secret/config validation, portal host/member ID и smoke разрешённых Bitrix methods, включая crm.item.list для entityTypeId=3 с >=updatedTime, opened=1 и registration field =1;
  9. открыть на nginx ВМ2 только два exact webhook routes для version-controlled BITRIX_WEBHOOK_ALLOWED_CIDRS, выполнить nginx config test, valid/invalid source IP и query-token form-urlencoded contract tests, подтвердить отсутствие query/body в logs/traces и отсутствие запросов на ВМ1;
  10. включить sync, проверить обработку только post-watermark canary user, mapping в bitrix_sync, отсутствие CRM ID в App DB, suppression, alert/rebind и telemetry;
  11. наблюдать не менее agreed canary window queue age, CRM limit errors, DLQ, webhook/reconciliation lag, число Contact, восстановленных reconciliation без webhook, source-IP rejects и SigNoz alerts; scheduled full reconciliation не запускать.

Rollback: закрыть public webhook routes на nginx ВМ2 либо вернуть retryable 503, остановить claims, bounded drain in-flight, установить BITRIX_SYNC_ENABLED=false. ВМ1 не изменяется. Уже созданные Contact/mapping автоматически не удалять; pending после watermark не отменять; schema downgrade с production rows не выполнять.

Изменение source IP Битрикс24

Сигнал для проверки allow-list — всплеск Contact, восстановленных инкрементальной reconciliation без обработанного webhook, особенно одновременно с ростом webhook_rejected_total{reason="source_ip"}.

  1. Сопоставить окно всплеска с bounded-retention журналом source-IP rejects nginx ВМ2; query и body не извлекать и не сохранять.
  2. Подтвердить принадлежность нового адреса инфраструктуре Битрикс24/портала по согласованному каналу или контролируемым probe. Наличие корректного query token само по себе не является подтверждением.
  3. Добавить минимально необходимый IP/CIDR в version-controlled BITRIX_WEBHOOK_ALLOWED_CIDRS, выполнить peer review, manifest validation и nginx -t через штатный deployment unit.
  4. Применить safe reload, проверить приём Contact/alert webhook и отсутствие query/body в logs/traces.
  5. Убедиться, что source-IP rejects прекратились, webhook lag нормализовался, а следующие инкрементальные reconciliation run не показывают растущих восстановлений.
  6. При ошибочном расширении немедленно вернуть предыдущую approved версию allow-list. Автоматическое добавление наблюдаемого IP запрещено.

14. Stage 11 — Keycloak bootstrap

14.1. Первый старт

Запустить PostgreSQL-ready Keycloak отдельно:

cd <BACKEND_ROOT>
docker compose up -d keycloak
docker compose ps keycloak
docker compose logs --since=10m keycloak

Bootstrap admin secret существует только на первый запуск. После создания named admin с MFA удалить/ротировать bootstrap credential из runtime env.

14.2. Realm

Clean environment может импортировать secret-free han-chat realm template. Живой production realm нельзя перетирать --import-realm без diff.

Проверить:

  • client han-chat-frontend, public, PKCE S256;
  • direct/implicit/password/social disabled;
  • audience han-chat-api;
  • exact redirect/web origins;
  • issuer https://<PUBLIC_HOST>/auth/realms/han-chat;
  • claims sub, phone_number, verified, audience;
  • custom phone OTP provider;
  • settings bridge token/path;
  • mock code non-default и не виден UI/log;
  • brute-force, token/session TTL, refresh rotation;
  • admin console limited by VPN/allow-list.

14.3. Provider migration

Custom OTP tables мигрируются versioned mechanism до включения flow. Не редактировать standard Keycloak tables вручную.

Gate 11

  • Discovery/JWKS public через HTTPS.
  • Issuer exact, no internal hostname.
  • Realm drift check clean.
  • Только Authorization Code + PKCE S256.
  • OTP wrong/replay/limit tests fail safely.
  • Settings bridge cache/fail-closed tested.
  • Bootstrap admin removed; named admin MFA enabled.

15. Stage 12 — ordered startup и health gates

Архитектурный порядок:

  1. На ВМ2 approved systemd unit поднимает Redis Safety и local Collector.
  2. Затем clamd/freshclam, Safety API/worker и bitrix-sync.
  3. Последним на ВМ2 поднимается nginx с независимыми public 80/443 и private 8443 server blocks; проверяются оба TLS-контура, exact webhook routes, capability health, signature age и отрицательные ingress/egress tests.
  4. На ВМ1 unit поднимает локальные 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 из старого stub-контура не является evidence готовности target ВМ2.

При повторной раскатке reload после readiness upstream обязателен: nginx разрешает Docker DNS при загрузке конфигурации и иначе может продолжить обращаться к старому container IP. Bare-команды nginx -t и nginx -s reload не использовать: рабочий config/PID находятся в /tmp, а filesystem контейнера read-only.

После каждого шага ждать health, но проверять readiness отдельно из internal network:

docker compose exec -T api-backend python -c "<INTERNAL_HEALTH_PROBE_COMMAND>"

Не использовать host ports для curl. Допустим dedicated toolbox container в backend network.

Expected:

  • Redis PONG;
  • Keycloak DB/realm/provider ready;
  • Collector health + exporter queue;
  • Safety v2 capability ready и Redis Safety (target); legacy DB2 проверяется только в stub acceptance;
  • API DB/Redis/JWKS/settings/S3/Safety ready;
  • local app до Bitrix install может быть portal_not_installed;
  • bitrix-sync до enablement возвращает sync_disabled; после preflight — mode=full, validated settings/secrets/grants, живые worker/limiter и актуальные reconciliation cursors;
  • nginx config test success.

Gate 12

  • Все containers live, нет restart loop/OOM.
  • Critical readiness green.
  • Expected degraded statuses только документированные; sync stub mode отсутствует.
  • docker compose ps не публикует internal ports.
  • Internal /internal/* снаружи 404.
  • OTEL принимает telemetry.

16. Stage 13 — Bitrix24 local app и Open Lines

Bitrix admin создаёт local application:

  • install URL https://<PUBLIC_HOST>/bitrix/install;
  • handler URL https://<PUBLIC_HOST>/bitrix/handler;
  • placement URL https://<PUBLIC_HOST>/bitrix/placement;
  • required scopes по module-06;
  • client id/secret загружены в secret store до install;
  • portal/domain allow-list exact.

Выполнить install в Bitrix24. Local app должен сохранить encrypted OAuth, затем:

  1. imconnector.register connector han_mobile_app;
  2. imconnector.activate line 8;
  3. event.bind;
  4. status/reconciliation.

Не использовать prototype /bitrix-internal/internal/v1/*: canonical path только internal Docker /internal/openlines/v1/*, наружу он отсутствует.

Проверить status из toolbox/internal network с Bearer token, не печатая token:

cd <BACKEND_ROOT>
docker compose run --rm --no-deps <TOOLBOX_SERVICE> \
  <SAFE_STATUS_PROBE_COMMAND>

Создать test dialog/message через public API, не прямым legacy payload с телефоном. Проверить mapping и ответ оператора.

Gate 13

  • OAuth stored encrypted; token не в logs.
  • Connector configured/active on line 8.
  • Events bound exactly once.
  • Outbound text reaches Open Lines once.
  • Operator reply reaches API, затем delivery ack.
  • Duplicate callback не создаёт duplicate message.
  • Internal status с internet недоступен.

17. Stage 14 — public smoke и E2E

17.1. Независимые public ingress

curl -I http://<PUBLIC_HOST>/
curl -fsS https://<PUBLIC_HOST>/api/v1/public/app-config
curl -fsS https://<PUBLIC_HOST>/api/v1/public/content
curl -fsS https://<PUBLIC_HOST>/auth/realms/han-chat/.well-known/openid-configuration
curl -i https://<PUBLIC_HOST>/internal/safety/v2/messages/check
openssl s_client -connect <PUBLIC_HOST>:443 -servername <PUBLIC_HOST>
curl -I http://<PROCESSING_PUBLIC_HOST>/
curl -i https://<PROCESSING_PUBLIC_HOST>/internal/sync/v1/status
curl -i https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/contact
openssl s_client -connect <PROCESSING_PUBLIC_HOST>:443 -servername <PROCESSING_PUBLIC_HOST>

Expected ВМ1: 308; public 200 strict DTO; discovery 200; internal 404; valid cert. Expected ВМ2: HTTP redirect/ACME policy, internal 404, GET webhook 405/404, valid отдельный cert. Valid/invalid POST webhook проверяется отдельным form-urlencoded contract test с разрешённого и запрещённого source IP без помещения query token в shell history или логи.

17.2. Auth/frontend

  • guest открывает content без write;
  • protected write без JWT → 401;
  • consent → mock OTP → PKCE tokens;
  • bootstrap не передаёт phone body;
  • session-start создаёт ux_session_id;
  • silent refresh работает без OTP;
  • logout очищает tokens;
  • wrong/replayed OTP не выдаёт tokens.
  • real mode: durable order возвращает sms_message_id до Direct response; callback обновляет только SMS journal; resend делает старый challenge superseded.

17.3. Message Safety правила stub

Обязательные E2E:

  • text, начинающийся после normalization с ф/Ф → public 422 message_blocked, Bitrix не вызван;
  • legacy stub-only: text с цифры → v1 203/test terminal 400; target v2 smoke использует 202/200|403|terminal 503, клиент internal pending не получает;
  • прочий text → allow;
  • terminal stub 400 преобразуется в 422, не в generic validation;
  • timeout → 503/504, checkpoint/recovery, без duplicate;
  • один slow poll не блокирует другие requests.

Статус message-safety должен быть явно stub; для real production он не заменяет antivirus/file scan.

17.4. Files

  • allow image/PDF ≤ configured size;
  • wrong extension+MIME/oversize/checksum reject;
  • direct presigned PUT quarantine;
  • allow promote attachments;
  • deny остаётся вне data и quarantine cleanup;
  • Safety read-only credential не может write;
  • download URL owner-only + audit;
  • presigned URL отсутствует в logs.

17.5. Realtime и ownership

  • WS connects/subscribes;
  • operator reply arrives;
  • reconnect + REST gap reconciliation;
  • polling fallback;
  • чужие dialog/message/attachment/document id → 404;
  • idempotency same body replay, changed body 409;
  • rate limits 429 + Retry-After.

Gate 14

  • Полный first-send flow успешен.
  • Safety allow/deny/pending/timeout проверены.
  • Text/file/WS/polling работают.
  • Ownership и no-public-internal tests зелёные.
  • Нет secret/PII/message body/presigned URL в logs.
  • Audit events созданы.
  • Bitrix получает только allowed message.

18. Stage 15 — observability validation

Следовать module-09-observability.md:

  1. послать request с известным X-Request-ID;
  2. найти nginx log, API trace и downstream spans;
  3. проверить service.name, environment, trace/request/UX ids;
  4. при выбранном telemetry backend проверить dashboards всех services; до выбора — проверить bounded stdout/debug acceptance и явно зафиксировать ограничение;
  5. trigger safe synthetic 4xx/5xx и проверить alert route, если backend с alerting уже выбран;
  6. при настроенном remote OTLP временно блокировать его, проверить bounded queue и business continuity;
  7. проверить Collector health/drop/refused;
  8. выполнить PII/secret canary test.

Gate 15

  • Три сигнала доступны.
  • Trace cross-service связан.
  • Для выбранного backend alerts доставляются on-call и SLO queries возвращают данные; иначе limitation/TBD явно принят и traffic не называется production-ready.
  • Collector outage не ломает business path.
  • Redaction test пройден.

19. Stage 16 — opening traffic

До открытия:

  • удалить maintenance response;
  • включить HSTS после финального TLS test;
  • сохранить release SHA/image digests/schema revisions/realm desired version;
  • создать restore point;
  • подтвердить on-call;
  • не удалять previous images;
  • observation window 60 минут.

В первые 60 минут: 5xx, auth, message delivery, DB/Redis, memory, restart, Collector queue, Bitrix OAuth/DLQ.

Gate 16

  • Все Gate 0–15 подписаны.
  • Rollback release доступен.
  • Backup/restore evidence свежий.
  • Нет active page alert.
  • Product owner принял stub limitations.

20. Backup и restore

PostgreSQL

  • provider daily + PITR;
  • перед migrations/Keycloak upgrade — manual restore point;
  • ежеквартальный restore clone;
  • проверить все schemas, Alembic/Keycloak revisions, keys, grants.

S3

  • versioning/lifecycle data buckets;
  • inventory/checksum при поддержке;
  • restore не делает objects public;
  • quarantine не является backup.

Redis

AOF/RDB ускоряют restart, но не business backup. При corruption поднять clean Redis; API восстанавливает durable state из PG. Никогда не считать Redis dump достаточным для messages/audit.

Keycloak

DB backup включает realm/users/signing keys/provider data. Secret-free realm export — config backup, не полный data backup. После restore проверить issuer/JWKS/PKCE/OTP/refresh.

Restore rehearsal

  1. isolated VPC/hostnames;
  2. restore PG clone и S3 copies;
  3. deploy same image digests;
  4. не направлять production DNS/Bitrix callbacks;
  5. run migrations only if required release;
  6. smoke auth/chat without real operator impact;
  7. record measured RPO/RTO;
  8. destroy isolated secrets/resources controlled.

21. Rollback

Application-only

  1. объявить incident/maintenance;
  2. при SMS incident вернуть KEYCLOAK_OTP_MOCK_ENABLED=true, прекратить новые real orders и сохранить journal/in-flight state;
  3. сохранить diagnostics и current state;
  4. остановить новые claims/send при возможности;
  5. переключить image tags на previous digests;
  6. не выполнять Alembic downgrade;
  7. SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true deployment/scripts/rollback.sh <PREVIOUS_IMMUTABLE_RELEASE>;
  8. health/smoke/idempotency;
  9. проверить outbox/inbox/SMS pending/uncertain/recovery.

Используется текущий runtime secret set и текущий несекретный config. Snapshot старого .env не создаётся и не применяется.

После backward-incompatible migration

Обычный rollback запрещён. Выбор:

  • forward-fix;
  • restore PG PITR + coordinated S3/Bitrix reconciliation;
  • maintenance до решения.

Нельзя rollback DB отдельно от Keycloak signing/session state без анализа.

TLS/nginx rollback

nginx -t; оставить старый config/workers при failure. Не удалять working cert. При ошибке renewal current cert остаётся, но alert.

Gate rollback

  • Previous images available.
  • Schema совместима.
  • No duplicate outbound after restart.
  • Data reconciliation completed.
  • Incident timeline содержит release/request ids без PII.

22. Routine operations

Ежедневно автоматически:

  • health/synthetics/SLO/alerts;
  • PG backup/PITR status;
  • TLS expiry/renew;
  • disk/inodes/OTEL queue;
  • Redis AOF/memory/evictions;
  • DLQ/backlog/quarantine age;
  • Keycloak signing/OAuth/settings cache.

Еженедельно:

  • vulnerability/image updates review;
  • failed login/rate-limit trend;
  • Bitrix connector desired/observed;
  • S3 lifecycle/inventory;
  • restore/rollback artifacts availability.

Ежемесячно:

  • patch OS/images in maintenance;
  • secret/access review;
  • capacity/cardinality/cost;
  • stale users/admins;
  • runbook sample drill.

Команды:

cd <BACKEND_ROOT>
docker compose ps
docker compose logs --since=15m <SERVICE>
docker stats --no-stream
docker system df
docker compose exec -T nginx nginx -t

Не использовать unbounded logs, docker system prune -a, Redis KEYS/FLUSH* или ad-hoc DB DELETE.

23. Incident commands

Безопасный triage:

cd <BACKEND_ROOT>
date -Is
docker compose ps
docker stats --no-stream
docker compose logs --since=10m --tail=500 <SERVICE>
df -h
free -h
sudo ss -lntp
sudo iptables -L HAN-CHAT-DOCKER -n -v

PG:

psql "<OPS_READONLY_DSN>" -c "select now(), count(*) from pg_stat_activity;"

Redis — только ops ACL:

docker compose exec -T redis redis-cli --user <OPS_USER> --pass '<FROM_SECURE_INPUT>' PING

Никогда не вставлять secret literal в ticket/chat. Предпочесть stdin/secret file. Не выполнять ручной replay message/DLQ до проверки idempotency и ambiguous Bitrix outcome.

Типовые сценарии:

  • API 503: DB/Redis/JWKS/settings/Safety readiness и circuits;
  • send timeout: safety checkpoint/outbox, не повторять с новым key;
  • Bitrix down: OAuth/circuit/backlog/DLQ, reads оставить;
  • Redis loss: clean restart, durable fallback, polling;
  • PG outage: не restart storm; provider incident;
  • disk full: остановить ingest growth, очистить только known cache/old image после inventory;
  • cert near expiry: webroot/DNS/rate limit, staging rehearsal;
  • secret leak: revoke/rotate, telemetry deletion, redeploy.

24. Upgrades

Общий порядок:

  1. release notes/security advisories;
  2. compatibility matrix;
  3. backup/PITR;
  4. staging clone;
  5. image/build/test/scan;
  6. expand migration;
  7. one service at a time по dependency order;
  8. health/E2E/observation;
  9. contract migration later;
  10. record digests/revisions.

Keycloak: не пропускать unsupported majors; проверить SPI/provider migration и JWKS. Redis: AOF compatibility/rewrite. Collector: config validate against exact version. nginx: nginx -t и TLS scan. PostgreSQL major upgrade сначала rehearsal clone.

25. Disaster recovery

Потеря VM

  1. provision new Ubuntu VM в VPC;
  2. применить reviewed hardening;
  3. attach public IP/update DNS с low TTL;
  4. restore secrets из vault, не со старого disk без проверки;
  5. pull exact images;
  6. mount/create volumes; Redis можно clean;
  7. connect existing/restored PG/S3;
  8. TLS issue/restore safely;
  9. ordered startup/gates;
  10. Bitrix callback/connectors verify;
  11. public smoke, then traffic.

Потеря PG

Restore PITR в new managed instance, private SG/TLS, update DSN, validate schemas/grants/revisions. Остановить writes до chosen restore point/reconciliation. S3 objects после restore point могут стать orphan; выполнить audit-backed reconcile.

Потеря S3

Без data backup/versioning полное восстановление невозможно. Временно отключить file operations, оставить text flow, restore objects/inventory, reconcile DB metadata, не генерировать URLs отсутствующих objects.

Compromise

Isolate VM, preserve forensic snapshot, rotate all service/DB/S3/Bitrix/Keycloak secrets, revoke sessions/signing keys по масштабу, deploy clean VM/images, restore trusted data, notify по incident/legal process.

26. Teardown cautions

docker compose down не удаляет managed PG/S3, но может остановить callbacks. down -v удалит Redis/ACME/OTEL queue volumes и запрещён без approval. Запрещены:

docker compose down -v
docker system prune -a --volumes
DROP DATABASE / DROP SCHEMA
S3 recursive delete
cloud project/VPC delete
certbot delete active cert

Перед teardown:

  • export inventory/digests/config without secrets;
  • revoke Bitrix app/callbacks;
  • revoke/rotate credentials;
  • backup/retention/legal hold;
  • DNS drain;
  • deletion protection removal — отдельное approval;
  • проверить shared VPC/PG/S3;
  • зафиксировать evidence уничтожения.

27. Definition of Done

  • VM/VPC/DNS/SG/hardening соответствуют Gate 1–2;
  • managed PG private/TLS/backups/least privilege/migrations работают;
  • S3 private/IAM/CORS/lifecycle проверены;
  • exact release/images/frontend deployed;
  • 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;
  • container hardening и сетевые границы соответствуют arch-06;
  • для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
  • Redis/Collector volumes/resources/security работают;
  • Keycloak realm/provider/PKCE/OTP готов;
  • ordered startup/readiness пройден;
  • Bitrix connector line 8 и callback flow проверены;
  • full auth/text/file/realtime/safety E2E зелёный;
  • observability/redaction проверены; SLO/alerts проверены для выбранного backend либо явно остаются принятым production-blocking TBD;
  • backup restore и rollback rehearsed;
  • ops/incident/upgrade/DR owners назначены;
  • все assumptions/TBD приняты до открытия traffic.

VM2 cutover, rollback, reprovision и Freshclam

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.

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 минимум дважды в год.

Message Safety config activation

Первая migration создаёт message_safety.config_versions и seed version 1; readiness не открывается без ровно одной valid active version. Config-only rollout выполняется отдельным root-owned migration/config job под config-admin DB role: создать immutable draft, проверить JSON Schema/cross-field constraints и наличие rules/detector artifacts, записать approvals, затем транзакционно activate. Runtime API/worker имеют только SELECT к config table.

После activation operator проверяет health config_version, text/link/file canaries, cache-key version и отсутствие изменения in-flight task version. Изменение, ослабляющее policy или увеличивающее limits, требует Security Owner; остальные — Safety Service Owner и Operations Owner. Rollback не реактивирует retired row: предыдущий payload клонируется в новую monotonic version и активируется с отдельным audit event.

Emergency MOCK

deploy может без root login включить/изменить/выключить mode:

sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false
sudo /usr/local/sbin/han-message-safety-mode standard

Root устанавливает helper и /etc/sudoers.d/deploy-message-safety-mode при bootstrap. Sudoers разрешает deploy только этот immutable root:root 0755 executable; helper имеет строгий parser без shell eval/path arguments, атомарно обновляет root:han-message-safety 0640 /etc/han-chat/message-safety-mode.env (dedicated GID 10001 доступен только non-root контейнеру), валидирует Compose config, выполняет фиксированную Message Safety API recreate/restart operation внутри root project и проверяет private health. На ошибке он восстанавливает предыдущий mode/config и повторяет restart. deploy не получает write к config, systemd units, Compose и Docker socket.

Перед включением operator фиксирует incident/change ID и выбранные text/file policies; после команды проверяет processing_mode=mock, forced canaries, отсутствие 202, metric/active alert и audit actor. Ранее принятые standard tasks сохраняют mode и завершаются без переклассификации; только новые requests используют MOCK. Автоматического timeout нет: mode действует без ограничения по времени до явного standard. Поэтому перед закрытием incident обязателен возврат в standard, проверка normal capabilities и text/link/EICAR canary. File, разрешённый в MOCK, маркируется scan_status=bypassed, а не clean.

freshclam имеет controlled egress только к утверждённому signature CDN. Seed max_signature_age_hours равен 240 ч (10 дней), а schema запрещает значения выше 720 ч (30 дней); stale/failed update выключает только files и поднимает alert. Новая база проходит integrity/load/EICAR canary и atomic activate/reload; при regression возвращается последняя валидная база.

Initial ВМ2: 4 vCPU/8 GiB/80 GiB, resource/PID limits и backpressure. Workers масштабируются первыми по queue depth, clamd — scan lanes. ВМ3/scale-out инициируются при sustained CPU/RAM >70%, queue age >30 с, провале module-05 performance gates, contention bitrix-sync или независимом release cadence.

28. Допущения, TBD и архитектурные конфликты

Допущения

  • D-A1: отдельные public hosts ВМ1/ВМ2; processing host публикует только CRM webhook, остальные сервисы ВМ2 остаются private.
  • D-A2: обязательный минимум — otel-collector; доступность remote backend не предполагается до закрытия D-TBD11, local Grafana stack не обязателен.
  • D-A3: managed provider даёт private network, TLS, backups/PITR.
  • D-A4: Bitrix portal/connector/line остаются разрешёнными значениями architecture.
  • D-A5: mock OTP временно разрешён до controlled SMS cutover как documented risk.

TBD до production

  • D-TBD1: реальные domains, Expo native redirect URI и Bitrix placement frame ancestors.
  • D-TBD2: final VM1/PG sizing, public API/WS SLO и общие RPO/RTO; Safety load/latency gates уже зафиксированы, monthly availability SLO Safety в MVP намеренно не вводится.
  • D-TBD3: legal retention/erasure для PG/S3/audit/telemetry.
  • D-TBD4: secret manager и rotation windows.
  • D-TBD5: реализовать/cutover production Safety v2; inbound operator files сознательно не проходят AV в MVP.
  • D-TBD6: реализовать код, migrations, portal fields/smart process/webhooks и выполнить bitrix-sync cutover по module-07.
  • D-TBD7: pinned Keycloak/nginx/Collector versions и SPI compatibility.
  • D-TBD8: exact migration/seed/toolbox CLI commands после реализации repo.
  • D-TBD9: Keycloak admin VPN/MFA topology.
  • D-TBD10: final CSP/CORS/S3 headers и cloud-specific IAM.
  • D-TBD11: уточнить auth/retention/alert route выбранного private SigNoz; backend и endpoint уже зафиксированы.

Обнаруженные конфликты

  1. module-07 теперь задаёт полный target bitrix-sync; до реализации кода/migrations/portal prerequisites сервис обязан оставаться disabled, документация сама по себе не означает выполненный cutover.
  2. Текущая implementation остаётся v1 stub; module-05 описывает draft target v2. Cutover является production gate.
  3. Прототипный PG init даёт runtime role CREATE schema и не разделяет migration/runtime roles; runbook требует ужесточения.
  4. Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot.
  5. Prototype публиковал /bitrix-internal/* и использовал /internal/v1/*; целевой контур это запрещает и использует /internal/openlines/v1/*.
  6. Runtime artifacts .env.example/Compose ещё могут не содержать зафиксированные VM2 env; документация не означает выполненный cutover.
  7. Точные RPO/RTO, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся app_settings, SMS journal по module-11 хранится бессрочно.
  8. init-managed-postgres.py по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.
  9. Текущие Compose/env/config artifacts могут ещё не содержать sms-service; документация не разрешает real mode до реализации и прохождения rollout gates.

29. Ссылки на прототип