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

1369 lines
79 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# module-10. Runbook развёртывания HAN Chat
> Статус: целевой runbook ВМ1/ВМ2. Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy.
> Все значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. Команды с `cd <BACKEND_ROOT>` требуют подстановки реального пути корня backend-репозитория на VM.
> Канонические источники: [`../architectory/README.md`](../architectory/README.md), [`../architectory/arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`../architectory/arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`../architectory/arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`../architectory/arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`../architectory/arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`../architectory/arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`../architectory/arch-06-service-hosting-security.md`](../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md)[`module-09-observability.md`](module-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 выполняются независимо.
11. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
12. 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:
```text
<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.
Проверка с рабочей станции:
```bash
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-сеанса.
Прототипный скрипт можно адаптировать:
```bash
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`](../../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. Проверки
```bash
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>`:
```bash
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:
```bash
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:
```json
[
{
"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:
```text
/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 и достаточно диска.
```bash
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. Создание
```bash
cd <BACKEND_ROOT>
umask 077
cp .env.example .env
chmod 600 .env
```
`.env` содержит только несекретный config и `SECRETS_SOURCE=file|selectel`.
Пароли, токены, access keys, credential-bearing DSN и `*_FILE` пути в нём
запрещены. Секреты выдаёт единый интерфейс:
```bash
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.
Пары должны совпасть:
```text
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;
```bash
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-вариант
```bash
cd <BACKEND_ROOT>
docker login <REGISTRY>
docker compose pull
docker image ls --digests
```
Registry token read-only и короткоживущий.
### Build-вариант
```bash
cd <BACKEND_ROOT>
DOCKER_BUILDKIT=1 docker compose build --pull
```
Build не получает production secrets. Записать image digests.
Frontend:
```bash
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
До запуска:
```bash
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.
Проверить:
```bash
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-выпуск:
```bash
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 использует:
```text
/etc/letsencrypt/live/<PUBLIC_HOST>/fullchain.pem
/etc/letsencrypt/live/<PUBLIC_HOST>/privkey.pem
```
### Phase B: TLS activation
```bash
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`:
```ini
[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`:
```ini
[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
```
Установка и обязательная проверка расписания:
```bash
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`](../../HAN_chat/bitrix-local-app/deploy/ssl-common.sh), [`ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/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
```bash
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
```bash
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 или:
```bash
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 отдельно:
```bash
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:
```bash
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:
```bash
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
```bash
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`](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.
Команды:
```bash
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:
```bash
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:
```bash
psql "<OPS_READONLY_DSN>" -c "select now(), count(*) from pg_stat_activity;"
```
Redis — только ops ACL:
```bash
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. Запрещены:
```text
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:
```bash
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. Max signature age 24 ч; 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. Ссылки на прототип
- Исторические шаги: [`../../HAN_chat/Deploy_steps.md`](../../HAN_chat/Deploy_steps.md).
- Исторические команды эксплуатации: [`../../HAN_chat/backend-managing.md`](../../HAN_chat/backend-managing.md).
- VM baseline: [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh).
- PG bootstrap: [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py), [`../../HAN_chat/deploy/init-managed-postgres.sql`](../../HAN_chat/deploy/init-managed-postgres.sql), [`../../HAN_chat/deploy/pg-init.env.example`](../../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-tohin.ru.site.conf), [`../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.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-issue.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh).