Files

24 KiB
Raw Permalink Blame History

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. Упорядоченный первый запуск

До preflight установите host Collector из pinned release artifact. Версию и SHA-256 возьмите из утверждённого release record, не из ответа GitHub API:

export OTEL_HOST_COLLECTOR_VERSION='0.117.0'
export OTEL_HOST_COLLECTOR_SHA256='90710c909a30fc3b89dd0e389c13f9e57ebbb87d6a0dc51fdde0bf03499a5f25'
deployment/scripts/setup-vm.sh
/usr/local/bin/otelcol-contrib validate \
  --config=deployment/observability/otel-host-collector.yaml

setup-vm.sh устанавливает и enable-ит unit, но не запускает host Collector. Он работает без Docker socket; offsets и exporter queue находятся в /var/lib/han-otel/host-collector.

Первый запуск выполняет 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
systemctl start han-host-otel-collector@production.service
systemctl --no-pager status han-host-otel-collector@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
ETAG="$(curl -fsSI https://<PUBLIC_HOST>/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://<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, повторный app-config 304, internal route 404, valid chain. В app-config проверьте mobile_update: Google Play и RuStore включены (latest_build=2, minimum_build=1, latest_version=1.0.1, RuStore URL https://www.rustore.ru/catalog/app/ru.han.chat), App Store отключён, его release-поля равны null; пустой release_notes у всех политик также возвращается как null. Проверьте 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.

В SigNoz один canary request должен дать ровно по одному application log от nginx/API/Bitrix, а trace_id/span_id application logs должны открывать соответствующий span. Platform logs ищутся по host.name, service.name и временному окну. Проверьте отсутствие unknown-container, дублей Python logs, логов обоих Collector и экспоненциального роста ingest. После restart host Collector старые записи не должны replay-иться: offsets сохраняются.

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:

systemctl is-enabled docker.service han-chat-docker-firewall.service \
  han-secrets@production.service han-stack@production.service \
  han-host-otel-collector@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
systemctl restart han-host-otel-collector@production.service

Повторите smoke и зафиксируйте digests. Не удаляйте current/previous release, active images, evidence, volumes или /var/lib/han-otel: offsets/queue должны пережить rollback. Если previous release не содержит совместимого host config, остановите host unit до возврата совместимой версии. После 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.