Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений

This commit is contained in:
mi
2026-08-19 18:24:00 +03:00
parent bbef7a30c9
commit c7a80e7256
103 changed files with 3457 additions and 3725 deletions
@@ -1,8 +1,12 @@
# Ранбук развёртывания Processing на VM2
Этот каталог — независимая основа VM2. Он не разворачивает VM1 и не
затрагивает `codebase/backend`. Все команды ниже — операторские; создание
репозитория их не выполняет.
Этот каталог — независимая основа VM2 в репозитории `VM2_services`. Он не
разворачивает VM1 и не затрагивает `VM1_app/codebase/backend`. Все команды
ниже — операторские; создание репозитория их не выполняет.
Корень репозитория ВМ2: `HAN_chat_specification/VM2_services`. Compose и
deployment-артефакты: `VM2_services/codebase/services/`. Локальные команды
ниже предполагают текущий каталог `VM2_services`, если не указано иное.
## Блокеры production перед первым запуском
@@ -64,6 +68,33 @@
не входит в `docker`/`lxd`; каждый вход и sudo-вызов считается инцидентной
операцией.
## Предварительные условия (до §1)
Этот runbook описывает операции **на уже созданной VM2**. До bootstrap
подготовьте вне Compose:
1. **Инфраструктура Selectel** — VPC/subnet, SG (`80/443/22` public;
`8443` только private; egress default-deny после bootstrap), sizing
(4 vCPU / 8 ГБ RAM / 80 ГБ SSD — см.
[`module-10-deployment-vm2.md`](../../../documentation/module-10-deployment-vm2.md)),
public и private IP VM2, DNS A-запись `PROCESSING_PUBLIC_HOST`.
2. **Managed PostgreSQL** — schemas/roles для `message_safety` и
`bitrix_sync`, отдельные migration/runtime DSN; см.
[`arch-10-deployment.md`](../../../../architectory/arch-10-deployment.md) §6.
3. **Образы** — собрать и push `han-message-safety`, `han-bitrix-sync`;
получить immutable digest для всех `*_IMAGE` в `.env.example` (nginx, redis,
clamav, otel-collector).
4. **Selectel Secrets Manager** — заполнить все remote names из
`deployment/secrets/config.example.json` (DSN, tokens, S3 read-only keys,
`REDIS_SAFETY_ACL`, internal TLS PEM для `8443`). Отдельный IAM principal
VM2 с read-only доступом только к этим именам.
5. **S3 quarantine bucket** и SigNoz OTLP endpoint — значения в `.env`.
6. **Internal TLS** — сертификат внутренней CA с SAN = private DNS VM2;
PEM хранится в Secrets Manager, не в каталоге релиза.
Порядок разделов §1–§5 → Gates 19 → §7 (post-acceptance). `systemctl enable`
и `systemctl start han-processing.service` — **только после успешного Gate 5**.
## 1. Bootstrap свежей VM2
На локальном компьютере один раз создайте **два разных** ключа. Закрытые части
@@ -85,8 +116,9 @@ break-glass оператору и храниться отдельно от deplo
каталог:
```powershell
# текущий каталог: ...\HAN_chat_specification\VM2_services
scp -i C:\Users\MI\.ssh\hansel `
.\HAN_chat_specification\codebase\services\deployment\scripts\setup-vm.sh `
.\codebase\services\deployment\scripts\setup-vm.sh `
root@<VM2_PUBLIC_IP>:/root/setup-vm2.sh
scp -i C:\Users\MI\.ssh\hansel `
C:\Users\MI\.ssh\han_vm2_deploy.pub `
@@ -161,8 +193,8 @@ sudo rm -f /root/han_vm2_deploy.pub /root/han_vm2_admin.pub
```
## 2. Передача релиза под `deploy`
На локальном компьютере из каталога `HAN_chat_specification`:
На локальном компьютере из каталога `VM2_services`:
```powershell
$Release = "<VERSION_OR_GIT_SHA>"
@@ -390,71 +422,16 @@ Nginx с primary GID `11001` получает только подготовле
`/etc/letsencrypt` остаётся доступен только root/Certbot. Не копируйте private
key в каталог релиза и не делайте его world-readable.
## 6. Preflight, миграции и первый запуск под `root`
## 6. Gates 19: preflight, миграции и первый запуск под `root`
Сначала синхронизируйте секреты. Затем выполните статический preflight:
```sh
systemctl start han-secrets-vm2.service
/opt/han-chat/services/deployment/preflight.sh
/usr/local/sbin/han-vm2-compose config --quiet
```
До runtime выполните миграции отдельными DB roles и активируйте начальный
Message Safety config:
Перед первым `bitrix-sync-migrate` владелец `han_app` или администратор БД
выдаёт Bitrix migration-role временный read-only доступ к legacy mapping:
```sql
GRANT USAGE ON SCHEMA han_app TO <BITRIX_SYNC_MIGRATION_ROLE>;
GRANT SELECT ON TABLE han_app.entity_external_mapping
TO <BITRIX_SYNC_MIGRATION_ROLE>;
```
```sh
/usr/local/sbin/han-vm2-compose --profile ops run --rm message-safety-migrate
/usr/local/sbin/han-vm2-compose --profile ops run --rm bitrix-sync-migrate
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
--entrypoint message-safety-config message-safety-migrate \
create /app/app/artifacts/seed-config.yaml --version 1 --actor '<OPERATOR>'
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
--entrypoint message-safety-config message-safety-migrate \
activate --version 1 --approved-by '<APPROVER>'
```
После успешного `bitrix-sync-migrate` администратор БД отзывает временные
права. Право `USAGE` отзывайте только если оно не требуется этой роли для
других согласованных операций:
```sql
REVOKE SELECT ON TABLE han_app.entity_external_mapping
FROM <BITRIX_SYNC_MIGRATION_ROLE>;
REVOKE USAGE ON SCHEMA han_app FROM <BITRIX_SYNC_MIGRATION_ROLE>;
```
Первый запуск и enable выполняет `root` только после прохождения gates:
(внутри gate5)
```sh
systemctl enable han-secrets-vm2.service han-processing.service
systemctl start han-processing.service
systemctl --no-pager status han-processing.service
journalctl --no-pager -u han-processing.service
```
Дальнейшие штатные операции может выполнить `deploy`:
```sh
sudo systemctl restart han-secrets-vm2.service
sudo systemctl restart han-processing.service
sudo systemctl --no-pager status han-processing.service
sudo journalctl --no-pager -u han-processing.service
```
Выполняйте gates **строго по порядку** 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9.
Не включайте `han-processing.service` и не делайте `systemctl enable`, пока
Gate 5 не завершился успешно.
Установка/редактирование unit, Compose, `.env`, secret mapping, credential,
TLS, allow-list и запуск migration jobs остаются операциями `root`.
После Gate 5 штатный restart/status/logs для `deploy` — см. блок
«Дальнейшие штатные операции» ниже.
### Gate 1 — секреты материализованы
@@ -490,7 +467,39 @@ deployment/preflight.sh
### Gate 3 — миграции и активный Message Safety config
Команды миграций из предыдущего раздела выполняются под `root`. После них:
Под `root`. Перед первым `bitrix-sync-migrate` владелец `han_app` или
администратор БД выдаёт Bitrix migration-role временный read-only доступ к
legacy mapping:
```sql
GRANT USAGE ON SCHEMA han_app TO <BITRIX_SYNC_MIGRATION_ROLE>;
GRANT SELECT ON TABLE han_app.entity_external_mapping
TO <BITRIX_SYNC_MIGRATION_ROLE>;
```
```sh
/usr/local/sbin/han-vm2-compose --profile ops run --rm message-safety-migrate
/usr/local/sbin/han-vm2-compose --profile ops run --rm bitrix-sync-migrate
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
--entrypoint message-safety-config message-safety-migrate \
create /app/app/artifacts/seed-config.yaml --version 1 --actor '<OPERATOR>'
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
--entrypoint message-safety-config message-safety-migrate \
activate --version 1 --approved-by '<APPROVER>'
```
После успешного `bitrix-sync-migrate` администратор БД отзывает временные
права. Право `USAGE` отзывайте только если оно не требуется этой роли для
других согласованных операций:
```sql
REVOKE SELECT ON TABLE han_app.entity_external_mapping
FROM <BITRIX_SYNC_MIGRATION_ROLE>;
REVOKE USAGE ON SCHEMA han_app FROM <BITRIX_SYNC_MIGRATION_ROLE>;
```
Проверьте head revision и активную config version:
```sh
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
@@ -623,6 +632,16 @@ Message Safety; проверьте её без вывода секретов:
systemctl enable han-secrets-vm2.service han-processing.service
systemctl start han-processing.service
systemctl --no-pager status han-processing.service
journalctl --no-pager -u han-processing.service
```
Дальнейшие штатные операции может выполнить `deploy`:
```sh
sudo systemctl restart han-secrets-vm2.service
sudo systemctl restart han-processing.service
sudo systemctl --no-pager status han-processing.service
sudo journalctl --no-pager -u han-processing.service
```
### Gate 6 — host ports и сертификаты
@@ -811,47 +830,10 @@ unset CANARY
упавший receiver должен возвращать retryable `503`/закрытую маршрутизацию,
никогда успешный `2xx ignored`.
## Политика отказов
## 7. После Gate 9 — post-acceptance
- Отказ зависимости Safety — fail-closed: VM1 не должна отправлять/продвигать
контент.
- Устаревшие/недоступные сигнатуры ClamAV отключают только файловую
capability; ошибка сканирования никогда не превращается в allow.
- Потеря Redis может убрать ускорение, но PostgreSQL остаётся источником
истины.
- Сбой OTEL ставит в очередь в пределах ограниченного тома и не должен
менять вердикты.
- Rollback не понижает схемы, не удаляет durable tasks/mappings и не
запускает `docker compose down -v`.
## Аварийный MOCK
Разрешены только эти пять sudo-команд:
```text
han-message-safety-mode standard
han-message-safety-mode mock --text-free true --file-free true
han-message-safety-mode mock --text-free true --file-free false
han-message-safety-mode mock --text-free false --file-free true
han-message-safety-mode mock --text-free false --file-free false
```
Хелпер атомарно пишет только
`/etc/han-chat/message-safety-mode.env`, пересоздаёт только Safety API,
проверяет health и при сбое восстанавливает предыдущий режим. У MOCK нет
таймаута: держите high-severity alert активным до явного `standard`, затем
проверьте нормальные text/link/file capabilities и EICAR-canary.
## Известные исключения по образам
Образы ClamAV могут потребовать корректировок UID/path после валидации
точного digest. Не ослабляйте `read_only`, capabilities или mounts глобально:
задокументируйте минимальные writable пути для сигнатур/runtime и
компенсируйте сетевыми и ресурсными лимитами. Egress к signature-CDN
получает только `freshclam`; `clamd` — нет.
# Gate 9 завершает техническую приёмку VM2, но не означает production cutover сервисов.
Gate 9 завершает техническую приёмку VM2, но **не** означает production
cutover Message Safety на VM1 и **не** разрешает включать Bitrix sync.
Дальнейший порядок:
@@ -912,6 +894,237 @@ BITRIX_SYNC_ENABLED=false
BITRIX_SYNC_MODE=disabled
```
Public allow-list — только `deny all;`. Включать Bitrix можно лишь после выполнения gates `module-07`: поля портала, webhooks, migrations, grants, backfill/watermark и rollback rehearsal.
Public allow-list — только `deny all;`. Включать Bitrix можно лишь после
выполнения gates [`module-07-bitrix-sync.md`](../../../documentation/module-07-bitrix-sync.md):
поля портала, webhooks, migrations, grants, backfill/watermark и rollback
rehearsal.
Таким образом, ближайший шаг сейчас — reboot-gate и фиксация приёмки VM2. Затем переход к интеграции VM1, а не немедленное включение Bitrix.
После успешного Gate 9 выполните reboot-gate (п. 2 выше), зафиксируйте
приёмку (п. 3), затем выполните отдельный controlled cutover ниже. Gate 9 сам
по себе не разрешает переключать caller.
### Controlled cutover Message Safety на VM1
Cutover выполняется в согласованное окно совместно с Safety Service,
Rule Pack, Security, Product и Operations. До начала зафиксируйте текущий и
предыдущий schema-compatible immutable release VM1, ответственного за rollback
и stop conditions. `bitrix-sync` в это окно не включается.
#### 1. Предварительные условия
До изменения VM1 должны быть выполнены все условия:
- reboot-gate VM2 и повторные Gates 6–8 успешны;
- Safety работает в `standard`, не в `mock`;
- active config и rules bundle утверждены, Safety v2 migrations находятся на
ожидаемом head;
- `text`, `links`, `files`, `worker` имеют состояние `ready`;
- VM2 имеет только read-only доступ к versioned S3 quarantine objects;
- performance, egress negative tests и redaction Gate 9 закрыты;
- private DNS VM2 резолвится с VM1 только в private VPC address;
- security group/firewall разрешает `8443` от VM1 и approved ops, но не из
интернета;
- на VM1 подготовлен release без local `message-safety`, Redis DB2, local rules
env и stub fallback;
- один и тот же production service token подготовлен в раздельных secret
bundles VM1 и VM2; значение токена не печатается и не копируется в
`/etc/han/vm1.env`.
На VM1 под `root` повторите проверку TLS и readiness:
```sh
openssl s_client -connect <VM2_PRIVATE_IP>:8443 \
-servername <VM2_PRIVATE_DNS_NAME> \
-verify_hostname <VM2_PRIVATE_DNS_NAME> \
-CAfile /etc/han/ca/vm2-internal-ca.pem \
-verify_return_error </dev/null
curl --fail --silent --show-error \
--cacert /etc/han/ca/vm2-internal-ca.pem \
https://<VM2_PRIVATE_DNS_NAME>:8443/internal/safety/status
```
В TLS-выводе ожидается успешная проверка chain/SAN. В status ожидаются
`processing_mode=standard`, непустой `config_version` и
`text|links|files|worker=ready`. `stub`, `mock`, `not_ready` или недоступная
capability — stop condition.
#### 2. Прямой pre-cutover smoke с VM1
Прямой smoke доказывает route, CA и paired token до перезапуска caller:
```sh
SAFETY_TOKEN="$(cat <MESSAGE_SAFETY_SERVICE_TOKEN_FILE_ON_VM1>)"
MESSAGE_ID="$(uuidgen)"
curl --silent --show-error --write-out '\nHTTP %{http_code}\n' --config - <<EOF
url = "https://<VM2_PRIVATE_DNS_NAME>:8443/internal/safety/v2/messages/check"
cacert = "/etc/han/ca/vm2-internal-ca.pem"
request = "POST"
header = "X-Service-Token: ${SAFETY_TOKEN}"
header = "X-Request-ID: ${MESSAGE_ID}"
header = "Content-Type: application/json"
data = "{\"message_id\":\"${MESSAGE_ID}\",\"content_kind\":\"text\",\"text\":\"VM1 to VM2 cutover canary\",\"attachment\":null}"
EOF
unset SAFETY_TOKEN MESSAGE_ID
```
Ожидается `HTTP 200`, `verdict=allow`, `processing_mode=standard` и непустые
`config_version`/`rules_version`. Отдельный запрос с фейковым token marker
должен вернуть `401`; production token для negative test не изменяйте.
До переключения также выполните через API VM2:
- deny smoke на утверждённом безопасном corpus case — ожидается `403`;
- повтор запроса с тем же `message_id` и тем же body — тот же sticky result;
- тот же `message_id` с другим body — `409`;
- file smoke только с реальным versioned quarantine object:
`202 + Location + Retry-After`, затем terminal `200` или `403`;
- lease/fencing smoke с остановкой/возвратом worker по утверждённому test case:
task не исполняется двумя владельцами и сохраняет sticky terminal result.
Не используйте выдуманные S3 key/version/ETag и не загружайте EICAR в
production bucket вне согласованного security test.
#### 3. Переключение caller на VM1
На VM1 установите и проверьте internal CA по процедуре
[`RUNBOOK.production.ru.md`](../../../../VM1_app/codebase/backend/deployment/RUNBOOK.production.ru.md)
§6. В `/etc/han/vm1.env` должны быть:
```dotenv
MESSAGE_SAFETY_URL=https://<VM2_PRIVATE_DNS_NAME>:8443
MESSAGE_SAFETY_CA_HOST_PATH=/etc/han/ca/vm2-internal-ca.pem
MESSAGE_SAFETY_API_PREFIX=/internal/safety/v2
```
В root-owned Selectel secret mapping VM1 переменная
`MESSAGE_SAFETY_SERVICE_TOKEN` должна ссылаться на согласованный remote secret.
После review config и mapping:
```sh
cd /opt/han-chat/current/backend
./scripts/validate-env /etc/han/vm1.env
systemctl restart han-secrets@production.service
systemctl is-active han-secrets@production.service
journalctl --no-pager -u han-secrets@production.service
./deployment/preflight.sh
/usr/local/sbin/han-vm1-compose config --quiet
/usr/local/sbin/han-vm1-compose config --services
/usr/local/sbin/han-vm1-compose config --images
```
В `config --services` не должно быть local `message-safety`, `bitrix-sync`,
Redis DB2 или test stub. Не сохраняйте resolved Compose в файл и не выводите
secret values. Если preflight успешен, переключите caller:
```sh
systemctl restart han-stack@production.service
systemctl is-active han-stack@production.service
/usr/local/sbin/han-vm1-compose ps
```
#### 4. Post-cutover проверки через caller
Проверки выполняются через public API/штатный UI VM1, а не только прямым curl к
VM2:
1. benign text проходит Safety и отправляется ровно один раз;
2. утверждённый deny text не отправляется в Bitrix и возвращает клиенту generic
`message_blocked` без internal `rule_id`;
3. сообщение с безопасной HTTP/HTTPS-ссылкой проходит, запрещённая
private/link-local/metadata ссылка блокируется без HTTP fetch этой ссылки;
4. реальный quarantine file проходит `pending` и terminal result, после allow
продвигается штатным caller flow; deny-файл не продвигается;
5. повтор client/idempotency request не создаёт второе сообщение или второй
Safety task;
6. correlation request ID виден в VM1, VM2 и SigNoz без текста сообщения,
token, object key и других секретов.
Затем согласованным способом кратко сделайте VM2 недоступной **только для
test request** и подтвердите fail-closed: VM1 не отправляет и не продвигает
контент, возвращает контролируемую retryable ошибку, а local/stub fallback не
активируется. Сразу восстановите доступ и повторите benign smoke. Не имитируйте
отказ остановкой всей VM2, если на ней уже есть другой production traffic.
#### 5. Rollback rehearsal
Rollback caller — только на заранее проверенный предыдущий
schema-compatible immutable release VM1 по
[`RUNBOOK.production.ru.md`](../../../../VM1_app/codebase/backend/deployment/RUNBOOK.production.ru.md)
§12. Он не меняет nginx VM2, не понижает schema и не удаляет уже созданные
Safety tasks:
```sh
PREVIOUS='<PREVIOUS_COMPATIBLE_GIT_SHA>'
test -d "/opt/han-chat/releases/${PREVIOUS}/backend"
ln -s "releases/${PREVIOUS}" /opt/han-chat/.current-new
mv -Tf /opt/han-chat/.current-new /opt/han-chat/current
systemctl daemon-reload
systemctl restart han-secrets@production.service
/opt/han-chat/current/backend/deployment/preflight.sh
systemctl restart han-stack@production.service
```
После rehearsal повторите public smoke, верните approved current release тем же
атомарным способом и снова повторите smoke. Потеря VM2 не разрешает fail-open,
переключение на local stub или обход Safety. При несовместимой migration
rollback запрещён: используйте forward fix либо заранее согласованный recovery
plan.
#### 6. Фиксация cutover
Сохраните без secret values:
- VM1/VM2 release и image digests, Safety schema head;
- private certificate fingerprint/expiry, `config_version` и `rules_version`;
- результаты allow/deny/pending/file/timeout/fail-closed/idempotency checks;
- firewall counters и доказательство недоступности `8443` с запрещённого
source;
- traces/log search и результат redaction canary;
- фактическое время переключения и rollback rehearsal;
- approvals Safety Service, Rule Pack, Security, Product и Operations.
Только после успешного выполнения всех пунктов Message Safety cutover считается
завершённым. Cutover `bitrix-sync` остаётся отдельным изменением по
[`module-07-bitrix-sync.md`](../../../documentation/module-07-bitrix-sync.md).
## Политика отказов
- Отказ зависимости Safety — fail-closed: VM1 не должна отправлять/продвигать
контент.
- Устаревшие/недоступные сигнатуры ClamAV отключают только файловую
capability; ошибка сканирования никогда не превращается в allow.
- Потеря Redis может убрать ускорение, но PostgreSQL остаётся источником
истины.
- Сбой OTEL ставит в очередь в пределах ограниченного тома и не должен
менять вердикты.
- Rollback не понижает схемы, не удаляет durable tasks/mappings и не
запускает `docker compose down -v`.
## Аварийный MOCK
Разрешены только эти пять sudo-команд:
```text
han-message-safety-mode standard
han-message-safety-mode mock --text-free true --file-free true
han-message-safety-mode mock --text-free true --file-free false
han-message-safety-mode mock --text-free false --file-free true
han-message-safety-mode mock --text-free false --file-free false
```
Хелпер атомарно пишет только
`/etc/han-chat/message-safety-mode.env`, пересоздаёт только Safety API,
проверяет health и при сбое восстанавливает предыдущий режим. У MOCK нет
таймаута: держите high-severity alert активным до явного `standard`, затем
проверьте нормальные text/link/file capabilities и EICAR-canary.
## Известные исключения по образам
Образы ClamAV могут потребовать корректировок UID/path после валидации
точного digest. Не ослабляйте `read_only`, capabilities или mounts глобально:
задокументируйте минимальные writable пути для сигнатур/runtime и
компенсируйте сетевыми и ресурсными лимитами. Egress к signature-CDN
получает только `freshclam`; `clamd` — нет.