# Fresh production runbook ВМ1 HAN Chat Это единственный исполняемый production-runbook ВМ1. Он предназначен только для новой Ubuntu 24.04 VM: in-place преобразование старой single-VM/stub инсталляции запрещено. Команды выполняет оператор; repository automation их не запускает. Запрещены: реальные секреты в репозитории, `.env`, argv/history или логах; доступ `deploy` к Docker; mutable image tags; `docker compose down -v`; Alembic downgrade; автоматический fallback Selectel → локальный файл. ## 0. Участники, переменные и stop conditions - локальная машина: создаёт проверенный архив и SHA-256; - `root`: bootstrap, активация release, config/secrets/TLS, миграции и первый запуск; - `deploy`: пишет только в `/var/lib/han-deploy/incoming`, затем использует exact systemd/status/log commands; - `admin`: персональная break-glass роль с отдельным ключом и sudo-паролем. До окна работ зафиксируйте: ``, image digests, ``, DNS/IP ВМ1, private DNS/SAN ВМ2, ops CIDR, PG/S3 inventory, PITR marker, RPO/RTO, on-call, approver и предыдущий совместимый release. Stop condition: любой placeholder, незакрытый preflight, несовпавший digest, невалидный TLS, broad SG/sudo, неизвестная Alembic revision, stub/local Safety, неуспешный negative probe или отсутствие rollback evidence. ## 1. Bootstrap новой ВМ1 На локальной машине создайте две разные Ed25519 key pairs. Private keys не передаются на VM и не должны совпадать с bootstrap root key: ```powershell ssh-keygen -t ed25519 -a 100 -f C:\Users\\.ssh\han_vm1_deploy ` -C "han-vm1-deploy" ssh-keygen -t ed25519 -a 100 -f C:\Users\\.ssh\han_vm1_admin ` -C "han-vm1-break-glass-admin" ``` Передайте setup и только public keys во временный root-каталог. В уже открытой bootstrap root-сессии: ```sh install -d -m 0700 -o root -g root /root/bootstrap install -m 0600 -o root -g root /tmp/han_vm1_deploy.pub /root/bootstrap/deploy.pub install -m 0600 -o root -g root /tmp/han_vm1_admin.pub /root/bootstrap/admin.pub install -m 0700 -o root -g root /tmp/setup-vm.sh /root/setup-vm1.sh DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \ ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \ /root/setup-vm1.sh passwd admin ``` Setup устанавливает host packages, Docker/Compose, UFW, fail2ban, security updates, `DOCKER-USER`, swap и роли. Он не запускает Compose. Cloud SG должен разрешать `80/443` из интернета. На bootstrap-этапе UFW временно разрешает SSH из любой сети; после настройки WireGuard закройте public SSH и разрешите его только через WireGuard. PG принимает TLS только от SG/private IP ВМ1. `6379`, `4317/4318`, `8000`, `8080`, `9000` наружу запрещены. Не закрывая root-сессию, проверьте в двух новых сессиях: ```powershell ssh -i C:\Users\\.ssh\han_vm1_deploy deploy@ ssh -i C:\Users\\.ssh\han_vm1_admin admin@ ``` В admin-сессии: ```sh sudo -v sudo -i id exit ``` Только после успеха обоих SSH-входов и admin sudo повторите в исходной root-сессии: ```sh DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \ ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \ HARDEN_SSH=true SKIP_APT_UPGRADE=true \ /root/setup-vm1.sh ``` Откройте ещё по одной новой deploy/admin сессии после reload. Подтвердите `PermitRootLogin no`, `AllowUsers deploy admin`, key-only auth и отсутствие forwarding. Только затем закрывайте bootstrap root session. Root-only public key files сохраните до установки helpers из первого release; private keys на VM отсутствуют. Доступ cloud console/recovery остаётся break-glass. ## 2. Сборка и передача immutable release На локальной машине checkout должен быть exact detached ``, tree — clean. Архив содержит один корень `backend`, не содержит `.env`, credentials, caches и private keys: ```powershell $Release = "" tar --exclude=backend/.env ` --exclude='backend/**/__pycache__' ` --exclude='backend/**/.pytest_cache' ` --exclude='backend/**/.ruff_cache' ` -czf "vm1-backend-$Release.tar.gz" ` -C .\VM1_app\codebase backend Get-FileHash "vm1-backend-$Release.tar.gz" -Algorithm SHA256 scp "vm1-backend-$Release.tar.gz" ` deploy@:/var/lib/han-deploy/incoming/ ``` Под `deploy` разрешены только inventory/checksum: ```sh RELEASE='' cd /var/lib/han-deploy/incoming sha256sum "vm1-backend-${RELEASE}.tar.gz" tar -tzf "vm1-backend-${RELEASE}.tar.gz" ``` ## 3. Root-активация: SHA и anti-traversal Под `admin`, затем `sudo -i`. Не распаковывайте недоверенный архив до всех проверок: ```sh RELEASE='' EXPECTED_SHA256='' ARCHIVE="/var/lib/han-deploy/incoming/vm1-backend-${RELEASE}.tar.gz" printf '%s %s\n' "$EXPECTED_SHA256" "$ARCHIVE" | sha256sum --check - tar -tvzf "$ARCHIVE" if tar -tzf "$ARCHIVE" | grep -Eq '(^/|(^|/)\.\.(/|$)|^backend/\.env$)'; then echo 'unsafe path or .env' >&2; exit 1 fi if tar -tzf "$ARCHIVE" | grep -Ev '^backend(/|$)' | grep -q .; then echo 'archive has files outside backend' >&2; exit 1 fi if tar -tvzf "$ARCHIVE" | awk '$1 ~ /^[lh]/ {found=1} END {exit !found}'; then echo 'symlink/hardlink is forbidden' >&2; exit 1 fi TARGET="/opt/han-chat/releases/${RELEASE}" test ! -e "$TARGET" install -d -m 0755 -o root -g root "$TARGET" tar --extract --gzip --file "$ARCHIVE" --directory "$TARGET" \ --no-same-owner --no-same-permissions test -f "$TARGET/backend/docker-compose.yml" chmod 0755 \ "$TARGET/backend/deployment/scripts/setup-vm.sh" \ "$TARGET/backend/deployment/preflight.sh" \ "$TARGET/backend/deployment/scripts/tls-deploy-hook.sh" \ "$TARGET/backend/deployment/secrets/han-compose" \ "$TARGET/backend/deployment/secrets/han-secrets" test -x "$TARGET/backend/deployment/scripts/setup-vm.sh" test -x "$TARGET/backend/deployment/preflight.sh" test -x "$TARGET/backend/deployment/scripts/tls-deploy-hook.sh" test -x "$TARGET/backend/deployment/secrets/han-compose" test -x "$TARGET/backend/deployment/secrets/han-secrets" if find "$TARGET/backend" -type l -print -quit | grep -q .; then exit 1; fi chown -R root:root "$TARGET" chmod -R go-w "$TARGET" ln -s "releases/${RELEASE}" /opt/han-chat/.current-new mv -Tf /opt/han-chat/.current-new /opt/han-chat/current ``` Executable modes поставляет release manifest/git. Нельзя применять blanket `--chmod=F644` или рекурсивный `chmod`, снимающий executable bit. Сохраните SHA-256 архива и release SHA в change record. Повторите setup из активного release с теми же public key files. Он установит root-owned launcher, secret unit, stack unit, TLS hook и sudoers, но не запустит приложение: ```sh DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \ ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \ HARDEN_SSH=true SKIP_APT_UPGRADE=true \ /opt/han-chat/current/backend/deployment/scripts/setup-vm.sh visudo -cf /etc/sudoers.d/deploy sudo -l -U deploy rm -f /root/bootstrap/deploy.pub /root/bootstrap/admin.pub ``` Убедитесь, что в выводе нет wildcard, shell/editor/cp/chmod/docker и что `deploy` не входит в `docker`, `sudo`, `lxd`, `adm`, `systemd-journal`. Для повторного setup после будущего release заново передайте только проверенные public keys в root-only временный каталог и удалите их после выполнения. ## 4. Несекретный config и secrets Под root создайте `/etc/han/vm1.env` из reviewed production template. Config живёт вне immutable release и не меняется при rollback. В нём только несекретные значения и paths; `APP_ENV=production`, `SECRETS_SOURCE=selectel`, все images pinned `@sha256:...`, `MESSAGE_SAFETY_URL=https://:8443`, `MESSAGE_SAFETY_EXTRA_HOST==`, `MESSAGE_SAFETY_CA_HOST_PATH=/etc/han/ca/vm2-internal-ca.pem`, `NGINX_TLS_CERTIFICATE=/run/tls/fullchain.pem`, `NGINX_TLS_CERTIFICATE_KEY=/run/tls/privkey.pem`. ```sh cd /opt/han-chat/current/backend install -m 0600 -o root -g root .env.example /etc/han/vm1.env editor /etc/han/vm1.env ./scripts/validate-env /etc/han/vm1.env install -m 0600 -o root -g root deployment/secrets/config.example.json \ /etc/han/secrets/production.selectel.json editor /etc/han/secrets/production.selectel.json ``` Mapping должен содержать только VM1 secrets. Убедитесь, что в нём отсутствуют legacy local `message-safety`/`bitrix-sync` consumers, Redis DB2 и credentials сервисов ВМ2; отдельный IAM principal ВМ1 получает read-only только к перечисленным remote names. Пароли/DSN/token/S3 keys не помещаются в `/etc/han/vm1.env`. Создайте encrypted systemd credential без значения в argv/history: ```sh read -rsp 'Selectel VM1 service-user password: ' SELECTEL_PASSWORD; echo printf '%s' "$SELECTEL_PASSWORD" | systemd-creds encrypt \ --name=selectel-service-user-password - \ /etc/han/credentials/production.selectel-password.cred unset SELECTEL_PASSWORD chown root:root /etc/han/credentials/production.selectel-password.cred chmod 0600 /etc/han/credentials/production.selectel-password.cred ``` Подтвердите token pairs без печати значений средствами validator. Не делайте `cat` runtime secret files. Emergency file mode — отдельная root-only процедура без automatic fallback. ## 5. Managed PostgreSQL и S3 Установите provider CA вне release: ```sh install -m 0644 -o root -g root /tmp/ \ /etc/han/ca/managed-postgresql-ca.pem openssl x509 -in /etc/han/ca/managed-postgresql-ca.pem \ -noout -subject -issuer -dates rm -f /tmp/ ``` Runtime и migration roles разделены для `han_app`, `bitrix_local`, `sms`, `keycloak`; runtime не имеет DDL/ownership. DSN проверяет hostname и chain. Включены encryption, deletion protection, backup/PITR и alerts. Перед миграциями создаётся provider PITR marker. S3: private encrypted buckets documents/attachments/quarantine; exact browser origin CORS; prefix-scoped VM1 key; versioning/lifecycle; public ACL off. Read-only quarantine key Safety принадлежит IAM ВМ2 и не копируется на ВМ1. Проверьте negative access к чужому prefix/bucket. ## 6. Public TLS и internal VM2 CA Установите внутренний CA, которым ВМ1 проверяет SAN ВМ2: ```sh install -m 0644 -o root -g root /tmp/ \ /etc/han/ca/vm2-internal-ca.pem openssl x509 -in /etc/han/ca/vm2-internal-ca.pem \ -noout -subject -issuer -dates rm -f /tmp/ openssl s_client -connect :8443 \ -servername -verify_hostname \ -CAfile /etc/han/ca/vm2-internal-ca.pem -verify_return_error /dev/null unset NGINX_ID systemctl enable han-secrets@production.service han-stack@production.service systemctl start han-stack@production.service ``` `han-stack@production` становится единственным routine lifecycle interface. После изменения secret source сначала explicit restart secret unit, затем stack unit. Обновление active/exited oneshot всегда требует `restart`. ## 10. Smoke, firewall и cutover С внешней машины: ```sh curl -sS -o /dev/null -w '%{http_code}\n' http:/// curl -fsS https:///api/v1/public/app-config ETAG="$(curl -fsSI https:///api/v1/public/app-config | awk -F': ' 'tolower($1)=="etag" {gsub("\r","",$2); print $2}')" curl -sS -o /dev/null -w '%{http_code}\n' \ -H "If-None-Match: $ETAG" https:///api/v1/public/app-config curl -fsS https:///auth/realms/han-chat/.well-known/openid-configuration curl -sS -o /dev/null -w '%{http_code}\n' \ https:///internal/safety/v2/messages/check openssl s_client -connect :443 -servername \ -verify_hostname -verify_return_error ' \ --authenticator webroot --webroot-path /var/lib/han-chat/acme certbot renew --dry-run --run-deploy-hooks systemctl enable --now certbot.timer systemctl list-timers certbot.timer ``` Hook должен завершаться `0` с пустым stderr на success, атомарно обновлять staging, выполнять config test и HUP. Проверьте logs/metrics/traces, request ID через nginx/API/VM2, alerts для 5xx/auth/Safety/PG/Redis/OOM/disk/OTEL queue/TLS. Отправьте только fake canary token/PII markers и докажите их отсутствие в logs/traces. KESL 12.4 устанавливается и принимается только по отдельному операторскому runbook `deployment/kesl/RUNBOOK.KESL.ru.md`. Не совмещайте установку, полную/контейнерную антивирусную проверку или изменение File Threat Protection с deploy, миграциями, PG backup, TLS renewal и перезапуском Docker/стека. Изменение политики KESL не является частью обычного application release. Перед reboot проверьте admin SSH и provider console: ```sh systemctl is-enabled docker.service han-chat-docker-firewall.service \ han-secrets@production.service han-stack@production.service certbot.timer systemctl reboot ``` После reconnect повторите status, stack `ps`, public/private TLS, smoke, negative ports и firewall counters. Без reboot gate deployment не завершён. ## 12. Rollback и disaster recovery Application rollback допускается только на предыдущий root-owned release, совместимый с текущей schema. Секреты и `/etc/han/vm1.env` остаются текущими: ```sh PREVIOUS='' 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 ``` Повторите smoke и зафиксируйте digests. Не удаляйте current/previous release, active images, evidence или volumes. После incompatible migration используйте forward fix либо согласованный PG PITR + S3/Bitrix reconciliation в maintenance window; Redis восстанавливается пустым и прогревается из PG. Потеря ВМ1: создайте новую VM этим runbook, восстановите DNS/SG, root-owned approved release, VM1 IAM secrets, managed PG/S3 и public TLS; ВМ2 не пересоздавайте. До cutover держите traffic закрытым. Ежеквартально делайте isolated restore rehearsal, не направляя production DNS/Bitrix callbacks, и фиксируйте фактические RPO/RTO. При компрометации host не «очищайте» VM: изолируйте, сохраните evidence, ротируйте доступные secrets/tokens и reprovision из trusted image. ## 13. Production acceptance record Сохраните без secret values: release/archive SHA-256, image digests, schema heads, realm/config version, PG PITR marker, TLS fingerprints/expiry, systemd status, positive/negative firewall probes, public/private smokes, VM2 cutover approval, observability canary, reboot result, rollback/restore rehearsal и подписи Operations/Security/Product.