21 KiB
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-паролем.
До окна работ зафиксируйте: <GIT_SHA>, image digests, <EXPECTED_SHA256>,
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:
ssh-keygen -t ed25519 -a 100 -f C:\Users\<USER>\.ssh\han_vm1_deploy `
-C "han-vm1-deploy"
ssh-keygen -t ed25519 -a 100 -f C:\Users\<USER>\.ssh\han_vm1_admin `
-C "han-vm1-break-glass-admin"
Передайте setup и только public keys во временный root-каталог. В уже открытой bootstrap root-сессии:
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-сессию, проверьте в двух новых сессиях:
ssh -i C:\Users\<USER>\.ssh\han_vm1_deploy deploy@<VM1_IP>
ssh -i C:\Users\<USER>\.ssh\han_vm1_admin admin@<VM1_IP>
В admin-сессии:
sudo -v
sudo -i
id
exit
Только после успеха обоих SSH-входов и admin sudo повторите в исходной root-сессии:
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 <GIT_SHA>, tree —
clean. Архив содержит один корень backend, не содержит .env, credentials,
caches и private keys:
$Release = "<GIT_SHA>"
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@<VM1_IP>:/var/lib/han-deploy/incoming/
Под deploy разрешены только inventory/checksum:
RELEASE='<GIT_SHA>'
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. Не распаковывайте недоверенный архив до всех
проверок:
RELEASE='<GIT_SHA>'
EXPECTED_SHA256='<SHA256_FROM_APPROVED_WORKSTATION>'
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, но не запустит приложение:
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://<VM2_PRIVATE_DNS>:8443,
MESSAGE_SAFETY_EXTRA_HOST=<VM2_PRIVATE_DNS>=<VM2_PRIVATE_IP>,
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.
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:
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:
install -m 0644 -o root -g root /tmp/<PG_CA_FILE> \
/etc/han/ca/managed-postgresql-ca.pem
openssl x509 -in /etc/han/ca/managed-postgresql-ca.pem \
-noout -subject -issuer -dates
rm -f /tmp/<PG_CA_FILE>
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:
install -m 0644 -o root -g root /tmp/<VM2_INTERNAL_CA_FILE> \
/etc/han/ca/vm2-internal-ca.pem
openssl x509 -in /etc/han/ca/vm2-internal-ca.pem \
-noout -subject -issuer -dates
rm -f /tmp/<VM2_INTERNAL_CA_FILE>
openssl s_client -connect <VM2_PRIVATE_IP>:8443 \
-servername <VM2_PRIVATE_DNS> -verify_hostname <VM2_PRIVATE_DNS> \
-CAfile /etc/han/ca/vm2-internal-ca.pem -verify_return_error </dev/null
Для initial public certificate DNS уже указывает на ВМ1, 80 свободен:
PUBLIC_HOST='<PUBLIC_HOST>'
ACME_EMAIL='<OPS_EMAIL>'
certbot certonly --standalone --preferred-challenges http --staging \
-d "$PUBLIC_HOST" --cert-name "${PUBLIC_HOST}-staging" \
--email "$ACME_EMAIL" --agree-tos --no-eff-email --non-interactive
certbot delete --cert-name "${PUBLIC_HOST}-staging" --non-interactive
certbot certonly --standalone --preferred-challenges http \
-d "$PUBLIC_HOST" --cert-name "$PUBLIC_HOST" \
--email "$ACME_EMAIL" --agree-tos --no-eff-email --non-interactive
При первом копировании nginx ещё не работает, поэтому deploy hook до запуска не вызывается. Скопируйте initial pair теми же root ownership и mode, затем preflight проверит pair:
install -m 0640 -o root -g han-nginx-tls \
"/etc/letsencrypt/live/${PUBLIC_HOST}/fullchain.pem" \
/var/lib/han-chat/public-tls/fullchain.pem
install -m 0640 -o root -g han-nginx-tls \
"/etc/letsencrypt/live/${PUBLIC_HOST}/privkey.pem" \
/var/lib/han-chat/public-tls/privkey.pem
В production Compose nginx обязан монтировать только
/var/lib/han-chat/public-tls read-only, не /etc/letsencrypt. Если это ещё
legacy named volume, preflight/Compose review — блокер, не workaround.
7. Secret sync и static preflight
systemctl restart han-secrets@production.service
systemctl is-active han-secrets@production.service
journalctl --no-pager -u han-secrets@production.service
test -s /run/han-chat/secrets/manifest
cut -d= -f1 /run/han-chat/secrets/manifest | sort
/opt/han-chat/current/backend/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
В списке production services после VM2 cutover нет local message-safety,
bitrix-sync, Redis DB2 и test stubs. Только nginx публикует 80/443; все
images immutable. Не сохраняйте resolved Compose с secret paths/metadata в
общедоступный файл.
8. Миграции и seed
После PITR marker под root:
/usr/local/sbin/han-vm1-compose --profile ops run --rm migrate-api
/usr/local/sbin/han-vm1-compose --profile ops run --rm migrate-bitrix-local
/usr/local/sbin/han-vm1-compose --profile ops run --rm migrate-sms
/usr/local/sbin/han-vm1-compose --profile ops run --rm seed-settings
/usr/local/sbin/han-vm1-compose --profile ops run --rm seed-settings
Проверьте current/head каждой схемы, идемпотентность seed и negative DDL от
runtime roles. Миграции ВМ1 han_app precede dependent VM2 sync cutover.
Временные cross-schema grants выдаёт owner и отзывает после проверки. Unknown
revision, multiple heads или incompatible schema — stop; ручной stamp и
downgrade запрещены.
9. Упорядоченный первый запуск
Первый запуск выполняет root через фиксированный launcher:
/usr/local/sbin/han-vm1-compose up -d redis otel-queue-init otel-collector
/usr/local/sbin/han-vm1-compose up -d keycloak sms-service
/usr/local/sbin/han-vm1-compose up -d api-backend bitrix-local-app
/usr/local/sbin/han-vm1-compose up -d \
sms-worker delivery-worker safety-recovery-worker cleanup-worker \
notification-expire-worker notification-draft-cleanup-worker
/usr/local/sbin/han-vm1-compose up -d frontend-static
/usr/local/sbin/han-vm1-compose up -d nginx
/usr/local/sbin/han-vm1-compose ps
Дождитесь health, затем:
/usr/local/sbin/han-vm1-compose exec -T nginx nginx -t -c /tmp/nginx.conf
NGINX_ID=$(/usr/local/sbin/han-vm1-compose ps --status running --quiet nginx)
test -n "$NGINX_ID"
docker kill --signal HUP "$NGINX_ID" >/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
С внешней машины:
curl -sS -o /dev/null -w '%{http_code}\n' http://<PUBLIC_HOST>/
curl -fsS https://<PUBLIC_HOST>/api/v1/public/app-config
curl -fsS https://<PUBLIC_HOST>/auth/realms/han-chat/.well-known/openid-configuration
curl -sS -o /dev/null -w '%{http_code}\n' \
https://<PUBLIC_HOST>/internal/safety/v2/messages/check
openssl s_client -connect <PUBLIC_HOST>:443 -servername <PUBLIC_HOST> \
-verify_hostname <PUBLIC_HOST> -verify_return_error </dev/null
Ожидается 308, public endpoints 200, internal route 404, valid chain.
Проверьте guest/auth PKCE/OTP, SMS mode, Open Lines, idempotency, ownership,
rate limits, WS reconciliation, S3 quarantine/promote/deny и Safety v2
allow/deny/pending/timeout. Safety status stub не принимается.
До переключения MESSAGE_SAFETY_URL ВМ2 должна закрыть собственные gates.
После переключения подтвердите private CA/SAN, service token, text|links|files
capabilities и fail-closed timeout. Local Safety/Redis DB2 не оставляются как
fallback. Cutover ВМ2 Bitrix sync — отдельное окно.
Проверьте DOCKER-USER live counters внешним positive 80/443 и negative
port/source probe:
iptables -L HAN-CHAT-DOCKER -n -v
systemctl restart han-chat-docker-firewall.service
systemctl restart docker.service
systemctl restart han-chat-docker-firewall.service
iptables -S HAN-CHAT-DOCKER
Allow rules должны использовать --ctorigdstport 80/443; UFW INPUT counter не
является доказательством published Docker ports.
11. Certbot, observability и reboot gate
После запуска nginx переключите renewal на webroot, который Compose монтирует
из /var/lib/han-chat/acme, затем:
certbot reconfigure --cert-name '<PUBLIC_HOST>' \
--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.
Перед reboot проверьте admin SSH и provider console:
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 остаются текущими:
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
Повторите 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.