1369 lines
79 KiB
Markdown
1369 lines
79 KiB
Markdown
# 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 МиБ + 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.
|
||
|
||
Проверка с рабочей станции:
|
||
|
||
```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).
|