83 KiB
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.md–module-09-observability.md.
1. Неподвижные правила
- Один root Compose project описывается в
<BACKEND_ROOT>; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группыdocker. - На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle.
- Application containers не имеют public host ports. Nginx ВМ1 публикует свой
80/443; nginx ВМ2 — отдельный80/443только для exact CRM webhook и private8443для Message Safety/internal access. /internal/*не маршрутизируется публично.- Managed PostgreSQL находится вне Compose, в той же VPC, без public IP.
- S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
- Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
- Миграции выполняются отдельными one-shot steps до новой версии приложения.
- Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
bitrix-syncвводится только после выполнения preflight/cutover gates module-07; до этогоBITRIX_SYNC_ENABLED=false, public webhook закрыт на edge.- На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо.
- ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
- 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 МиБ + 5–10 ГБ 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.
- Для published Docker ports allow rules сопоставляют original host
destination через
conntrack --ctorigdstport; positive/negative probes увеличивают counters нужных allow/deny rules после restart Docker и reboot. - После обновления firewall helper active
oneshot RemainAfterExitunit явно перезапущен;enable --nowне считается применением новой версии. - 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 адаптировать:
- создать шесть schemas:
han_app,bitrix_local,bitrix_sync,keycloak,message_safety,sms; - создать runtime roles;
- создать migration roles либо controlled admin job;
- schema owner = migration role;
- runtime:
USAGE, DML и sequence grants только на свои objects; ALTER DEFAULT PRIVILEGESот migration owner;- запретить чужие schemas и public schema create;
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:
api-backendAlembic владеетhan_app, triggers, seed;bitrix-local-appAlembic владеетbitrix_local;message-safetystub не создаёт PG tables до production implementation;bitrix-syncmigration role владеет schemabitrix_sync; api-backend Alembic отдельно мигрирует sharedhan_app.sync_queue, triggers и mapping;- Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations;
sms-serviceвладеет versioned migrations/seed schemasms; runtimesms_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 и release file
manifest (path, owner/group, mode, executable). Sync с blanket
--chmod=F644 запрещён: scripts, preflight и hooks устанавливаются
0755/0750 по manifest и проходят test -x до activation.
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, localotel-collector; - root Compose ВМ2: собственный public/private nginx,
message-safety-api,message-safety-worker,clamd,freshclam,bitrix-sync, Redis Safety, localotel-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.
Каждый otel-queue перед collector подготавливает idempotent
otel-queue-init (10001:10001 0700, без сети/secrets, только
CHOWN/FOWNER); collector стартует только после успешного one-shot.
Проверить:
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 проверены.
- OTEL ownership init завершился
0, а write-test проходит от collector UID; root collector/0777не используются. - Container resource limits/healthchecks заданы.
- Healthcheck-команды выполнены в exact pinned digests; отсутствуют
ExitCode 127и зависимости от несуществующихwget/curl. docker compose config --quietsuccess.
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
- DNS уже указывает на VM.
- Запустить nginx с bootstrap config: только
/.well-known/acme-challenge/и redirect; TLS block не требует отсутствующий cert. - Запустить 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 должен:
- взять
flock, чтобы исключить параллельные запуски; - выполнить
docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet; - при успешном обновлении проверить
docker compose exec -T nginx nginx -t -c /tmp/nginx.conf; - только после успешной проверки выполнить
docker compose kill -s HUP nginx; - записать структурированный результат и метрику времени до истечения;
- вернуть ненулевой exit code при ошибке, чтобы сработал alert;
- не удалять действующий сертификат при неуспешном renew.
- на success path вернуть
0с пустым stderr; benign outputnginx -tи signal command подавить/перенаправить, полную диагностику печатать только при ошибке.
Пример 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:
- применить App DB seed
otp.phone.code_length,otp.phone.ttl_seconds,otp.phone.sms_order_timeout_ms; - создать schema/role
sms, применить migrations и idempotent seedsms_setting/active approvedauth_otp; - в test environment развернуть
sms-service/worker с локальным mock Direct и выполнить contract/E2E; - получить production Direct
TOKEN_1, согласованные sender и template, отдельные callback credentials; - определить egress IP фактическим запросом из
sms-worker, подтвердить его статичность/NAT, записать в inventory и передать Direct для allowlist; - развернуть production
sms-service/worker и callback route, оставивKEYCLOAK_OTP_MOCK_ENABLED=true; - применить Keycloak expand migration/SPI, мигрировать старые challenges по module-11;
- выполнить provider smoke отдельной ops-командой на
<IDGTL_TEST_PHONE>; проверить journal, callback, redaction и отсутствие duplicate; - только после подписанных evidence переключить
KEYCLOAK_OTP_MOCK_ENABLED=false; - проверить 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:
- создать/проверить 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; - создать smart process «Конфликты синхронизации», поля/стадии/ответственного/SLA и активировать validated
bitrix_sync.settings; - завести отдельный входящий webhook технического пользователя с минимальными правами module-07 §13;
- настроить два 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 не создавать; - применить expand migrations
han_app, затемbitrix_sync, после чего выдать точечные GRANT и выполнить negative permission tests; - зафиксировать
cutover_watermarkи one-shot операцией отменить существующие до него pending/retry contact-задачи с причинойinitial_full_sync_cutover; - не создавать backfill: это утверждённое ограничение первого релиза;
- запустить image с sync disabled, проверить
/health/live, expectedsync_disabled, secret/config validation, portal host/member ID и smoke разрешённых Bitrix methods, включаяcrm.item.listдляentityTypeId=3с>=updatedTime,opened=1и registration field=1; - открыть на 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; - включить sync, проверить обработку только post-watermark canary user, mapping в
bitrix_sync, отсутствие CRM ID в App DB, suppression, alert/rebind и telemetry; - наблюдать не менее 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"}.
- Сопоставить окно всплеска с bounded-retention журналом source-IP rejects nginx ВМ2; query и body не извлекать и не сохранять.
- Подтвердить принадлежность нового адреса инфраструктуре Битрикс24/портала по согласованному каналу или контролируемым probe. Наличие корректного query token само по себе не является подтверждением.
- Добавить минимально необходимый IP/CIDR в version-controlled
BITRIX_WEBHOOK_ALLOWED_CIDRS, выполнить peer review, manifest validation иnginx -tчерез штатный deployment unit. - Применить safe reload, проверить приём Contact/alert webhook и отсутствие query/body в logs/traces.
- Убедиться, что source-IP rejects прекратились, webhook lag нормализовался, а следующие инкрементальные reconciliation run не показывают растущих восстановлений.
- При ошибочном расширении немедленно вернуть предыдущую 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
Архитектурный порядок:
- На ВМ2 approved systemd unit поднимает Redis Safety и local Collector.
- Затем
clamd/freshclam, Safety API/worker иbitrix-sync. - Последним на ВМ2 поднимается nginx с независимыми public
80/443и private8443server blocks; проверяются оба TLS-контура, exact webhook routes, capability health, signature age и отрицательные ingress/egress tests. - На ВМ1 unit поднимает локальные Redis/Collector, API, SMS, Keycloak, local app и edge nginx.
- Только 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, затем:
imconnector.registerconnectorhan_mobile_app;imconnector.activateline8;event.bind;- 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 делает старый challengesuperseded.
17.3. Message Safety правила stub
Обязательные E2E:
- text, начинающийся после normalization с
ф/Ф→ public422 message_blocked, Bitrix не вызван; - legacy stub-only: text с цифры → v1
203/test terminal400; 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:
- послать request с известным
X-Request-ID; - найти nginx log, API trace и downstream spans;
- проверить
service.name, environment, trace/request/UX ids; - при выбранном telemetry backend проверить dashboards всех services; до выбора — проверить bounded stdout/debug acceptance и явно зафиксировать ограничение;
- trigger safe synthetic 4xx/5xx и проверить alert route, если backend с alerting уже выбран;
- при настроенном remote OTLP временно блокировать его, проверить bounded queue и business continuity;
- проверить Collector health/drop/refused;
- выполнить 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
- isolated VPC/hostnames;
- restore PG clone и S3 copies;
- deploy same image digests;
- не направлять production DNS/Bitrix callbacks;
- run migrations only if required release;
- smoke auth/chat without real operator impact;
- record measured RPO/RTO;
- destroy isolated secrets/resources controlled.
21. Rollback
Application-only
- объявить incident/maintenance;
- при SMS incident вернуть
KEYCLOAK_OTP_MOCK_ENABLED=true, прекратить новые real orders и сохранить journal/in-flight state; - сохранить diagnostics и current state;
- остановить новые claims/send при возможности;
- переключить image tags на previous digests;
- не выполнять Alembic downgrade;
SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true deployment/scripts/rollback.sh <PREVIOUS_IMMUTABLE_RELEASE>;- health/smoke/idempotency;
- проверить 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
Общий порядок:
- release notes/security advisories;
- compatibility matrix;
- backup/PITR;
- staging clone;
- image/build/test/scan;
- expand migration;
- one service at a time по dependency order;
- health/E2E/observation;
- contract migration later;
- 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
- provision new Ubuntu VM в VPC;
- применить reviewed hardening;
- attach public IP/update DNS с low TTL;
- restore secrets из vault, не со старого disk без проверки;
- pull exact images;
- mount/create volumes; Redis можно clean;
- connect existing/restored PG/S3;
- TLS issue/restore safely;
- ordered startup/gates;
- Bitrix callback/connectors verify;
- 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
.envvalidated; runtime secrets разделены по сервисам и защищены; deployне имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units;- один root Compose и nginx на VM; ВМ1 и ВМ2 имеют независимые public 80/443, ВМ2 дополнительно private 8443; public ВМ2 ограничен exact CRM webhook;
- после Safety cutover ВМ1 не содержит local Safety/Redis DB2 и вызывает ВМ2 только по private HTTPS с проверенным CA bind;
- container hardening и сетевые границы соответствуют arch-06;
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
- Redis/Collector volumes/resources/security работают;
- 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.
Обязательный gate миграции legacy ВМ1
Перед переключением caller на ВМ2 существующий backend ВМ1 считается legacy и должен пройти отдельный migration gate:
- env validator принимает только production
MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443, требуетMESSAGE_SAFETY_CA_HOST_PATHи проверяет hostname/SAN; cross-host Docker hostname и plaintext запрещены; - internal CA расположен в едином root-owned staging path, а не под
deploy:deploy 0700; positive read-test проходит от UID/GIDapi-backend, negative — от постороннего UID; - local
message-safety, его Redis DB2 и локальные rules-version env удалены из target Compose/validator. Одновременный local и remote Safety запрещён; - release tree/Compose/unit принадлежат root,
deployисключён изdocker, установлен реальный root-owned stack systemd unit и permission preflight; - VM1
DOCKER-USERсопоставляет original published ports черезconntrack --ctorigdstport; counters подтверждены positive/negative probe после Docker restart и reboot; - images закреплены digest, release file modes подтверждены manifest,
rollback выбирает совместимые digests/config, а не только строковый
RELEASE_VERSION; - ordered startup из этого runbook заменяет legacy
docker compose up -dвсего стека; после readiness выполняется production nginx config test/reload; han-secretsи firewall oneshot после обновления явно перезапущены и проверены по timestamp/live state; TLS renewal success path имеет пустой stderr.
Ни старый deployment guide, ни успешный запуск legacy single-VM Compose не являются доказательством прохождения этого gate.
Rollback возвращает caller adapter/upstream ВМ1 на предыдущий immutable release. Уже созданные v2 tasks завершаются/reconcile по PG checkpoints; down-migration и удаление quarantine versions запрещены.
При потере ВМ2 fail-open запрещён. ВМ2 reprovision-ится из immutable image/config; secrets materialize под отдельным IAM, Redis поднимается пустым, migrations/capability/egress gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год.
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 уже зафиксированы.
Обнаруженные конфликты
module-07теперь задаёт полный targetbitrix-sync; до реализации кода/migrations/portal prerequisites сервис обязан оставаться disabled, документация сама по себе не означает выполненный cutover.- Текущая implementation остаётся v1 stub; module-05 описывает draft target v2. Cutover является production gate.
- Прототипный PG init даёт runtime role
CREATEschema и не разделяет migration/runtime roles; runbook требует ужесточения. - Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot.
- Prototype публиковал
/bitrix-internal/*и использовал/internal/v1/*; целевой контур это запрещает и использует/internal/openlines/v1/*. - Runtime artifacts
.env.example/Compose ещё могут не содержать зафиксированные VM2 env; документация не означает выполненный cutover. - Точные RPO/RTO, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся
app_settings, SMS journal по module-11 хранится бессрочно. init-managed-postgres.pyпо умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.- Текущие Compose/env/config artifacts могут ещё не содержать
sms-service; документация не разрешает real mode до реализации и прохождения rollout gates.
29. Ссылки на прототип
- Исторические шаги:
../../HAN_chat/Deploy_steps.md. - Исторические команды эксплуатации:
../../HAN_chat/backend-managing.md. - VM baseline:
../../HAN_chat/deploy/setup-vm-han-chat.sh. - PG bootstrap:
../../HAN_chat/deploy/init-managed-postgres.py,../../HAN_chat/deploy/init-managed-postgres.sql,../../HAN_chat/deploy/pg-init.env.example. - Legacy nginx:
../../HAN_chat/bitrix-local-app/deploy/nginx-tohin.ru.site.conf,../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf. - Legacy TLS:
../../HAN_chat/bitrix-local-app/deploy/ssl-issue.sh,../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh,../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh.