Files
han-app/support&knowlages/releases/#1.1 VM-service-deploy.md
T

40 KiB
Raw Blame History

Usefull commands

Скопировать файл по технологии через архив

cd C:\Users\MI\Documents\Assistent\HAN_chat_specification

$Hotfix = "bug-v15" tar -czf "vm2-$Hotfix.tar.gz" -C .\codebase services/deployment/scripts/setup-vm.sh Get-FileHash "vm2-$Hotfix.tar.gz" -Algorithm SHA256 scp -i C:\Users\MI\.ssh\han_vm2_deploy "vm2-$Hotfix.tar.gz" ` deploy@135.106.179.209:/var/lib/han-deploy/incoming/

services/docker-compose.yml HAN_chat_specification\codebase\services\docker-compose.yml HAN_chat_specification\codebase\services\deployment\scripts\setup-vm.sh

HOTFIX='bug-v15' EXPECTED_SHA256='BF85F75F45F63BFB8310C6E0B835276DE7EBF316180120F6383590C19E44FEE4' ARCHIVE="/var/lib/han-deploy/incoming/vm2-${HOTFIX}.tar.gz"

printf '%s %s\n' "$EXPECTED_SHA256" "$ARCHIVE" | sha256sum --check - tar -tzf "$ARCHIVE"

STAGING="$(mktemp -d /opt/han-chat/.nginx-hotfix.XXXXXX)" tar -xzf "$ARCHIVE" -C "$STAGING" --no-same-owner --no-same-permissions

install -m 0644 -o root -g root "$STAGING/services/deployment/scripts/setup-vm.sh" /opt/han-chat/services/deployment/scripts/setup-vm.sh

sed -i 's/\r$//' ./deployment/scripts/setup-vm.sh

find -type f -exec file {} ; | grep -i 'CRLF'

Перезапустить контейнер

/usr/local/sbin/han-vm2-compose up -d --force-recreate freshclam

Создание докер-образов

docker login cr.selcloud.ru

cd /mnt/c/Users/MI/Documents/Assistent/HAN_chat_specification/codebase/services/bitrix-sync docker build -t han-bitrix-sync:1.0.3 . REGISTRY=cr.selcloud.ru/han-images docker tag han-bitrix-sync:1.0.3 $REGISTRY/han-bitrix-sync:1.0.3 docker push $REGISTRY/han-bitrix-sync:1.0.3 docker image inspect $REGISTRY/han-bitrix-sync:1.0.3 --format '{{index .RepoDigests 0}}'

cd /mnt/c/Users/MI/Documents/Assistent/HAN_chat_specification/codebase/services/message-safety docker build -t han-message-safety:1.0.2 . REGISTRY=cr.selcloud.ru/han-images # ваш registry docker tag han-message-safety:1.0.2 $REGISTRY/han-message-safety:1.0.2 docker push $REGISTRY/han-message-safety:1.0.2 docker image inspect $REGISTRY/han-message-safety:1.0.2 --format '{{index .RepoDigests 0}}'

Чтобы собрать sha256 с остальных образов, их надо закачать

docker pull nginxinc/nginx-unprivileged:1.27 docker pull redis:7.4 docker pull clamav/clamav:1.4 docker pull otel/opentelemetry-collector-contrib:0.117.0

docker image inspect nginxinc/nginx-unprivileged:1.27 --format '{{index .RepoDigests 0}}' docker image inspect redis:7.4 --format '{{index .RepoDigests 0}}' docker image inspect clamav/clamav:1.4 --format '{{index .RepoDigests 0}}' docker image inspect otel/opentelemetry-collector-contrib:0.117.0 --format '{{index .RepoDigests 0}}'

Генерация ключей для новых пользователей и первичная настройка ВМ2

ssh-keygen -t ed25519 -a 100 -f C:\Users\MI.ssh\han_vm2_deploy -C "han-vm2-deploy" ssh-keygen -t ed25519 -a 100 -f C:\Users\MI.ssh\han_vm2_admin -C "han-vm2-break-glass-admin"

scp -i C:\Users\MI.ssh\hansel C:\Users\MI\Documents\Assistent\HAN_chat_specification\codebase\services\deployment\scripts\setup-vm.sh root@135.106.179.209:/root/setup-vm2.sh scp -i C:\Users\MI.ssh\hansel C:\Users\MI.ssh\han_vm2_deploy.pub C:\Users\MI.ssh\han_vm2_admin.pub root@135.106.179.209:/root/

На VM2 в текущей root-сессии задайте реальные CIDR. /0 скрипт отклоняет:

ssh -i C:\Users\MI\.ssh\hansel root@135.106.179.209
sed -i 's/\r$//' /root/setup-vm2.sh

install -d -m 0700 -o root -g root /root/bootstrap
install -m 0600 -o root -g root /root/han_vm2_deploy.pub /root/bootstrap/deploy.pub
install -m 0600 -o root -g root /root/han_vm2_admin.pub /root/bootstrap/admin.pub
chmod 0700 /root/setup-vm2.sh
DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \
ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \
VM1_PRIVATE_CIDRS='192.168.0.0/24' \
/root/setup-vm2.sh

1 В текущей root-сессии задайте admin отдельный сложный sudo-пароль. Он не разрешает password SSH: пароль нужен только после входа по admin key:

passwd admin

2 Не закрывая root-сессию, проверьте отдельные SSH-ключи deploy и admin. ssh -i C:\Users\MI.ssh\han_vm2_deploy deploy@135.106.179.209 ssh -i C:\Users\MI.ssh\han_vm2_admin admin@135.106.179.209 В сессии admin проверьте sudo -v и sudo -i, затем завершите root shell: sudo -v # проверить, что пароль admin принимается sudo -i # открыть root shell (приглашение обычно root@...) id # убедиться, что uid=0 exit # ← вот это «завершите root shell» — вернуться к admin@

Только после успешной проверки deploy, admin и sudo повторите на VM2

под root:

DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \
ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \
VM1_PRIVATE_CIDRS='192.168.0.0/24' \
HARDEN_SSH=true \
SKIP_APT_UPGRADE=true \
/root/setup-vm2.sh

Это добавит PermitRootLogin no и AllowUsers deploy admin. Публичные bootstrap-копии после проверки можно удалить под admin:

sudo rm -f /root/han_vm2_deploy.pub /root/han_vm2_admin.pub

Проверка наличия сетевого интерфейса для ВМ и его создание

ls -la /etc/netplan netplan get ip route

cat >/etc/netplan/60-private-network.yaml <<'EOF' network: version: 2 ethernets: eth1: dhcp4: false addresses: - 192.168.0.4/24 optional: true EOF

chmod 0600 /etc/netplan/60-private-network.yaml netplan generate netplan try

Подтвердите конфигурацию в течение тайм-аута. Затем проверьте: ip -br -4 address show eth1 ip route ping -c 3 192.168.0.1

nginx правим разрешенные адреса

nginx/allowlists/

Копируем проект

(Текущая команда копирует весь каталог ради простоты формирования единого архива, но технической необходимости в исходниках приложений нет)

На локальном компьютере из каталога HAN_chat_specification:

$Release = "1.0.3"
tar --exclude=services/.env `
  --exclude='services/**/__pycache__' `
  --exclude='services/**/.pytest_cache' `
  --exclude='services/**/.ruff_cache' `
  -czf "vm2-services-$Release.tar.gz" -C C:\Users\MI\Documents\Assistent\HAN_chat_specification\codebase services
Get-FileHash "vm2-services-$Release.tar.gz" -Algorithm SHA256
scp -i C:\Users\MI\.ssh\han_vm2_deploy "vm2-services-$Release.tar.gz" `
  deploy@135.106.179.209:/var/lib/han-deploy/incoming/

Под deploy на VM2 вычислите checksum. Значение должно совпасть с локальным:

RELEASE='1.0.2'
cd /var/lib/han-deploy/incoming
sha256sum "vm2-services-${RELEASE}.tar.gz"
tar -tzf "vm2-services-${RELEASE}.tar.gz"

На этом действия deploy с файлами заканчиваются. Не распаковывайте релиз через sudo и не копируйте его в production от имени deploy.

Активация и установка файлов под root

ssh -i C:\Users\MI.ssh\han_vm2_admin admin@135.106.179.209 sudo -i id Должно быть uid=0(root).

Под root ещё раз сверьте ожидаемый SHA-256 и список архива. Не продолжайте, если архив содержит абсолютные пути, .., symlink/hardlink или лишний проект:

RELEASE='1.0.3'
EXPECTED_SHA256='0FC8F2B85EC39EE41BD6D3E3789963A4D985B3E2E9638E50BEAF978EA6A46271'
ARCHIVE="/var/lib/han-deploy/incoming/vm2-services-${RELEASE}.tar.gz"
printf '%s  %s\n' "$EXPECTED_SHA256" "$ARCHIVE" | sha256sum --check -
tar -tvzf "$ARCHIVE"
if tar -tzf "$ARCHIVE" | grep -Eq '(^/|(^|/)\.\.(/|$)|^services/\.env$)'; then
  echo 'ОШИБКА: архив содержит небезопасный путь или .env' >&2
  exit 1
fi
if tar -tzf "$ARCHIVE" | grep -Ev '^services(/|$)' | grep -q .; then
  echo 'ОШИБКА: архив содержит файлы вне каталога services' >&2
  exit 1
fi
if tar -tvzf "$ARCHIVE" | awk '$1 ~ /^[lh]/ { found=1 } END { exit !found }'; then
  echo 'ОШИБКА: архив содержит symlink или hardlink' >&2
  exit 1
fi
install -d -m 0755 -o root -g root /opt/han-chat/services
STAGING="$(mktemp -d /opt/han-chat/.vm2-release.XXXXXX)"
tar --extract --gzip --file "$ARCHIVE" \
  --directory "$STAGING" --no-same-owner --no-same-permissions
test -f "$STAGING/services/docker-compose.yml"
rsync -a --delete --exclude=.env \
  --chown=root:root --chmod=D755,F644 \
  "$STAGING/services/" /opt/han-chat/services/
rm -rf -- "$STAGING"
chmod 0755 /opt/han-chat/services/deployment/preflight.sh

cd /opt/han-chat/services/ sudo apt update sudo apt install -y dos2unix find . -type f (
-name '.sh' -o -name '.py' -o -name '.service' -o -name '.sudoers' -o -name 'han-compose' -o -name 'han-secrets' -o -name 'han-message-safety-mode' -o -name '.yml' -o -name '.conf*' -o -name 'preflight.sh' -o -name '.yaml' -o -name '.md' -o -name '.toml' -o -name 'Dockerfile' -o -name '.acl*'
) -exec dos2unix {} + find -type f -exec file {} ; | grep -i 'CRLF' sudo apt remove -y dos2unix sudo apt purge -y dos2unix

Если перезалили

scp /opt/han-chat/services/deployment/scripts/setup-vm.sh /root/setup-vm2.sh chmod 0700 /root/setup-vm2.sh

Была проблема с каретками, полечилась так: (sed -i 's/\r$//'
/opt/han-chat/services/deployment/deploy-message-safety-mode.sudoers

install -m 0440 -o root -g root
/opt/han-chat/services/deployment/deploy-message-safety-mode.sudoers
/etc/sudoers.d/deploy-message-safety-mode

visudo -cf /etc/sudoers.d/deploy-message-safety-mode)

Повторите setup под root: теперь он установит helpers и units из активного релиза. Приложение всё ещё не запускается:

DEPLOY_AUTHORIZED_KEY_FILE=/root/bootstrap/deploy.pub \
ADMIN_AUTHORIZED_KEY_FILE=/root/bootstrap/admin.pub \
VM1_PRIVATE_CIDRS='192.168.0.0/24' \
HARDEN_SSH=true \
SKIP_APT_UPGRADE=true \
/root/setup-vm2.sh

Несекретная конфигурация и Selectel под root

APP_ENV определяет имя loader config. При значении из .env.example APP_ENV=production-like файл обязан называться /etc/han/secrets/vm2-production-like.selectel.json:

cd /opt/han-chat/services
install -m 0600 -o root -g root .env.example .env
editor .env

# я скопировал и вставил значения из локального
sed -i 's/\r$//' .env


install -m 0600 -o root -g root \
  deployment/secrets/config.example.json \
  /etc/han/secrets/vm2-production-like.selectel.json
editor /etc/han/secrets/vm2-production-like.selectel.json

sudo sed -i 's/\r$//' /etc/han/secrets/vm2-production-like.selectel.json ls /etc/han/secrets/vm2-production-like.selectel.json

Для заполнения секретов нужно создать сертификаты для ВМ1-ВМ2.

1. Создать CA (один раз)

На безопасной машине (не обязательно VM2):

mkdir -p ~/han-internal-ca && cd ~/han-internal-ca

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=HAN Internal CA" \
  -out ca.crt

ca.key храните только у себя; на ВМ не кладите.

2. Выпустить серверный сертификат VM2

openssl genrsa -out vm2-processing.key 2048

cat > vm2-processing.ext <<'EOF'
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = processing.internal
EOF

openssl req -new -key vm2-processing.key \
  -subj "/CN=processing.internal" \
  -out vm2-processing.csr

openssl x509 -req -in vm2-processing.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out vm2-processing.crt -days 825 -sha256 \
  -extfile vm2-processing.ext

Проверка:

openssl x509 -in vm2-processing.crt -noout -text | grep -A2 'Subject Alternative Name'

3. Разложить секреты

VM2 (в Selectel Secrets / файлы loader’а):

  • VM2_INTERNAL_TLS_CERTIFICATE ← содержимое vm2-processing.crt (можно fullchain: cert + ca.crt)
  • VM2_INTERNAL_TLS_PRIVATE_KEYvm2-processing.key

VM1: scp -i ~/.ssh/hansel ./han-internal-ca/ca.crt root@135.106.164.58:/tmp/ca.crt

На VM1 от root: install -d -o root -g root -m 0755 /opt/han-chat/backend/secrets/tls install -o root -g root -m 0644 /tmp/ca.crt
/opt/han-chat/backend/secrets/tls/processing-internal-ca.crt rm -f /tmp/ca.crt

ls /opt/han-chat/backend/secrets/tls/processing-internal-ca.crt

nano /opt/han-chat/backend/.env

MESSAGE_SAFETY_URL=https://processing.internal:8443 MESSAGE_SAFETY_CA_HOST_PATH=/opt/han-chat/backend/secrets/tls/processing-internal-ca.crt

PRIVATE NETWORK

В private DNS / hosts на VM1: echo '192.168.0.4 processing.internal' >> /etc/hosts

Зашифруйте пароль Selectel service user через systemd credentials, не помещая его в аргументы или history:

read -rsp 'Selectel VM2 service-user password: ' SELECTEL_PASSWORD; echo
printf '%s' "$SELECTEL_PASSWORD" | systemd-creds encrypt \
  --name=selectel-service-user-password - \
  /etc/han/credentials/vm2.selectel-password.cred
unset SELECTEL_PASSWORD
chown root:root /etc/han/credentials/vm2.selectel-password.cred
chmod 0600 /etc/han/credentials/vm2.selectel-password.cred

Активные nginx allow-list файлы редактирует только root; последней строкой обязательно остаётся deny all;. До cutover Bitrix public allow-list должен оставаться закрытым:

editor /opt/han-chat/services/nginx/allowlists/private-caller-allowlist.conf
install -m 0644 -o root -g root \
  /opt/han-chat/services/nginx/allowlists/bitrix-webhook-allowlist.conf.template \
  /opt/han-chat/services/nginx/allowlists/bitrix-webhook-allowlist.conf
chown root:root /opt/han-chat/services/nginx/allowlists/*.conf
chmod 0644 /opt/han-chat/services/nginx/allowlists/*.conf

Нужно заполнить два активных nginx allow-list на VM2 (не UFW из setup-vm.sh). Шаблоны лежат в nginx/allowlists/.

1. private-caller-allowlist.conf — обязательно для работы Safety

Кто может ходить на private API :8443 (VM1 → Message Safety / sync status).

Пример:

# allow приватный IP VM1
allow 192.168.0.10/32;
# опционально — ваш VPN/ops в private сети
# allow 192.168.0.50/32;
deny all;

Берёте приватный IP VM1 (тот же смысл, что VM1_PRIVATE_CIDRS). Последняя строка всегда deny all;.

2. bitrix-webhook-allowlist.conf — только перед включением Bitrix

Кто может бить в публичные webhook (/bitrix/sync/webhook/...).

До cutover ранбук говорит: оставить закрытым (deny all; без allow) — это нормально, пока BITRIX_SYNC_ENABLED=false.

Перед включением sync — IP/CIDR исходящих адресов Битрикс24 (или прокси), например:

allow 185.x.x.x/32;
deny all;

Где править

На VM2 после раскладки релиза:

/opt/han-chat/services/nginx/allowlists/private-caller-allowlist.conf
/opt/han-chat/services/nginx/allowlists/bitrix-webhook-allowlist.conf

Шаблоны-подсказки: *.conf.template. Активные .conf в git намеренно с одним deny all; — заполняете на сервере (или копируете из template и раскомментируете allow).

Это отдельный слой от UFW: UFW режет порт на хосте, nginx allow-list — кто дойдёт до location после TLS.

Сертификат PostgreSQL и первоначальный выпуск public TLS

CA управляемой PostgreSQL

Скачайте CA-сертификат кластера из панели провайдера и передайте его на VM2 во временный путь. Под root установите сертификат вне каталога релиза:

scp -i C:\Users\MI.ssh\han_vm2_deploy -r C:\Users\MI\Documents\job\HAN_new_life\HANapp\Production\sertificates\CA.pem deploy@135.106.179.209:/tmp/ca.pem

install -d -m 0755 -o root -g root /etc/han/ca
install -m 0644 -o root -g root \
  /tmp/ca.pem \
  /etc/han/ca/managed-postgresql-ca.pem
openssl x509 -in /etc/han/ca/managed-postgresql-ca.pem \
  -noout -subject -issuer -dates
rm -f /tmp/ca.pem

Первоначальный выпуск Let's Encrypt

PROCESSING_PUBLIC_HOST должен быть DNS-именем, A-запись которого уже указывает на публичный IP VM2. Сертификат на IP-адрес этим порядком не выпускается. Порт 80 должен быть разрешён в cloud firewall/UFW и пока не занят nginx.

Под root задайте значения только для текущей shell-сессии и подготовьте постоянный webroot:

PUBLIC_HOST=service4chat.han0107.ru
ACME_EMAIL=ap@han.ru
install -d -m 0755 -o root -g root /var/lib/han-chat/acme
getent ahostsv4 "$PUBLIC_HOST"
ss -lntp | grep -E ':80[[:space:]]' && {
  echo 'Порт 80 уже занят; остановите listener перед standalone-проверкой' >&2
  exit 1
} || true

Сначала проверьте ACME через staging CA. Этот сертификат nginx не использует:

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

После успешного staging-теста выпустите production-сертификат:

certbot certonly --standalone --preferred-challenges http \
  -d "$PUBLIC_HOST" \
  --cert-name "$PUBLIC_HOST" \
  --email "$ACME_EMAIL" \
  --agree-tos --no-eff-email --non-interactive
certbot certificates
test -s "/etc/letsencrypt/live/${PUBLIC_HOST}/fullchain.pem"
test -s "/etc/letsencrypt/live/${PUBLIC_HOST}/privkey.pem"

Nginx получает /etc/letsencrypt с host read-only и после этого может пройти Gate 4 и первый запуск. Не копируйте private key в каталог релиза.

Preflight, миграции и первый запуск под root

Сначала синхронизируйте секреты. Затем выполните статический preflight:

systemctl start han-secrets-vm2.service
/opt/han-chat/services/deployment/preflight.sh
/usr/local/sbin/han-vm2-compose config --quiet

для перезапуска секретов systemctl restart han-secrets-vm2.service systemctl show han-secrets-vm2.service -p ActiveState -p SubState -p Result -p ExecMainStartTimestamp -p ExecMainStatus

Перед первым bitrix-sync-migrate владелец han_app или администратор БД выдаёт Bitrix migration-role временный read-only доступ к legacy mapping:

GRANT USAGE ON SCHEMA han_app TO bitrix_sync_user;
GRANT SELECT ON TABLE han_app.entity_external_mapping
  TO bitrix_sync_user;


До runtime выполните миграции отдельными DB roles и активируйте начальный
Message Safety config:

```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>'

Потом права отзываем REVOKE SELECT ON TABLE han_app.entity_external_mapping FROM bitrix_sync_user; REVOKE USAGE ON SCHEMA han_app FROM bitrix_sync_user;

Первый запуск и enable выполняет root только после прохождения gates:

Установка/редактирование unit, Compose, .env, secret mapping, credential, TLS, allow-list и запуск migration jobs остаются операциями root.

GATES

Gate 1 — секреты материализованы

Под root на VM2:

systemctl restart han-secrets-vm2.service
systemctl is-active han-secrets-vm2.service
journalctl --no-pager -u han-secrets-vm2.service
test -s /run/han-chat/secrets/manifest
cut -d= -f1 /run/han-chat/secrets/manifest | sort

Ожидается active; журнал не содержит значений секретов; последняя команда показывает только имена всех ключей из mapping. Не выполняйте cat файлов секретов и не вставляйте реальные значения в terminal history.

Gate 2 — статический preflight и Compose

Под root на VM2:

cd /opt/han-chat/services
deployment/preflight.sh
/usr/local/sbin/han-vm2-compose config --quiet
/usr/local/sbin/han-vm2-compose config --services
/usr/local/sbin/han-vm2-compose config --images

Все команды должны завершиться с кодом 0. В списке services нет PostgreSQL, а все production images содержат @sha256:. Вывод полного resolved Compose в файл не сохраняйте.

Gate 3 — миграции и активный Message Safety config

Команды миграций из предыдущего раздела выполняются под root. После них:

/usr/local/sbin/han-vm2-compose --profile ops run --rm \
  message-safety-migrate current
/usr/local/sbin/han-vm2-compose --profile ops run --rm \
  bitrix-sync-migrate current

Ожидается по одной head revision каждого сервиса. create --version 1 выполняется только при первом развёртывании. Для следующего конфига используйте новый монотонный номер и отдельные значения --actor/--approved-by; повторно активировать старую версию нельзя. Alembic downgrade запрещён.

Gate 4 — конфигурация nginx до запуска

После выпуска public TLS в /etc/letsencrypt и материализации internal TLS secrets. В Selectel значения VM2_INTERNAL_TLS_CERTIFICATE и VM2_INTERNAL_TLS_PRIVATE_KEY сохраняются как исходный PEM с настоящими переводами строк, не как повторный base64 и не как строка с литералами \n. После изменения remote secret перезапустите han-secrets-vm2.service; preflight проверит формат PEM и соответствие certificate/key без вывода их содержимого:

/opt/han-chat/services/deployment/preflight.sh
/usr/local/sbin/han-vm2-compose run --rm --no-deps \
  -e MESSAGE_SAFETY_UPSTREAM_HOST=127.0.0.1 \
  -e BITRIX_SYNC_UPSTREAM_HOST=127.0.0.1 \
  nginx \
  nginx -t -c /etc/nginx/nginx.conf

Базовый nginx.conf подключает обязательный /etc/nginx/conf.d/10-vm2.conf, поэтому команда завершится ошибкой, если entrypoint не создал конфигурацию из шаблона. Временные значения upstream нужны только для проверки до первого запуска backend-контейнеров; production Compose подставляет DNS-имена сервисов. Ожидается syntax is ok и test is successful; ошибок conf.d is not writable и предупреждения о превышении open-file limit быть не должно. Ошибка отсутствующего сертификата является блокером, а не основанием временно убрать TLS.

Gate 5 — упорядоченный первый запуск

Под root на VM2:

/usr/local/sbin/han-vm2-compose up -d redis-safety otel-collector
/usr/local/sbin/han-vm2-compose up -d freshclam clamd
/usr/local/sbin/han-vm2-compose up -d \
  message-safety-api message-safety-worker
/usr/local/sbin/han-vm2-compose up -d \
  bitrix-sync bitrix-sync-worker bitrix-sync-reconciliation
/usr/local/sbin/han-vm2-compose up -d nginx
/usr/local/sbin/han-vm2-compose ps

Дождитесь healthy у сервисов с healthcheck. Не продолжайте при Restarting, unhealthy, OOM или неожиданном Exited. После успешного первого запуска передайте дальнейший lifecycle systemd:

systemctl enable han-secrets-vm2.service han-processing.service
systemctl start han-processing.service
systemctl --no-pager status han-processing.service

Gate 6 — host ports и сертификаты

Под root на VM2:

ss -lntp | grep -E ':(80|443|8443|6379|8080|4317|4318)[[:space:]]'
/usr/local/sbin/han-vm2-compose ps --format json | jq .

Ожидаются host listeners только nginx: public 80, 443 и private-bound 8443 на PROCESSING_PRIVATE_BIND_ADDRESS. 6379, container 8080 и OTLP 4317/4318 на host отсутствуют.

С доверенной рабочей станции проверьте public chain:

openssl s_client -connect service4chat.han0107.ru:443 \
  -servername service4chat.han0107.ru \
  -verify_hostname service4chat.han0107.ru -verify_return_error </dev/null

С VM1 или ops host, имеющего private route, проверьте internal chain и SAN:

openssl s_client -connect 192.168.0.4:8443 \
  -servername processing.internal \
  -verify_hostname processing.internal \
  -CAfile /opt/han-chat/backend/secrets/tls/processing-internal-ca.crt \
  -verify_return_error </dev/null

Обе команды должны завершить certificate verification без ошибки.

Переключите renewal с первоначального standalone на webroot, который nginx обслуживает по /.well-known/acme-challenge/. certbot reconfigure сам проверит новый способ через staging CA:

PUBLIC_HOST='service4chat.han0107.ru'
certbot reconfigure \
  --cert-name "$PUBLIC_HOST" \
  --authenticator webroot \
  --webroot-path /var/lib/han-chat/acme

Повторный setup после активации релиза устанавливает deploy-hook /etc/letsencrypt/renewal-hooks/deploy/han-processing-nginx: после успешного обновления он атомарно размещает certificate/key с группой han-nginx-tls в /var/lib/han-chat/public-tls, проверяет конфигурацию nginx и отправляет контейнеру HUP. Проверьте полный цикл и включите штатное расписание Certbot:

test -x /etc/letsencrypt/renewal-hooks/deploy/han-processing-nginx
certbot renew --dry-run --run-deploy-hooks
systemctl enable --now certbot.timer
systemctl --no-pager status certbot.timer
systemctl list-timers certbot.timer

certbot.timer проверяет необходимость продления дважды в сутки; сертификат перевыпускается только при приближении срока. Ошибка dry-run или deploy-hook — блокер. Итог Congratulations, all simulated renewals succeeded означает успех. Со старым hook сообщение Hook 'deploy-hook' ran with error output могло быть ложным: Compose писал успешный HUP как Killing/Killed, а успешный nginx -tsyntax is ok в stderr. Исправленный hook выводит config-test только при ошибке и отправляет HUP без progress-вывода. Порт 80 после этого остаётся доступен для HTTP-01 renewal.

Gate 7 — public routing

С внешней тестовой машины:

curl -sS -o /dev/null -w '%{http_code}\n' \
  http://service4chat.han0107.ru/not-a-route
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://service4chat.han0107.ru/not-a-route
curl -sS -o /dev/null -w '%{http_code}\n' \
  http://service4chat.han0107.ru/bitrix/sync/webhook/contact
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://service4chat.han0107.ru/internal/safety/status
curl -sS -o /dev/null -w '%{http_code}\n' -X GET \
  https://service4chat.han0107.ru/bitrix/sync/webhook/contact

Ожидаемые коды по порядку: 308, 404, 426, 404, 405. Для HTTPS используйте только валидный public certificate, без -k.

POST к webhook с адреса вне Bitrix allow-list должен получить 403; если cloud firewall настроен на drop, допустим timeout. Затем повторите с разрешённого source IP и заведомо неверным receiver token: upstream должен ответить 403, не 2xx.

Gate 8 — private API только с VM1/ops

Следующие команды выполняются на VM1 или approved ops host, не на VM2. Используйте private DNS/SAN и внутреннюю CA.

Safety status:

curl --fail --silent --show-error \
  --cacert /opt/han-chat/backend/secrets/tls/processing-internal-ca.crt \
  https://processing.internal:8443/internal/safety/status

Benign text check через тот же private listener:

SAFETY_TOKEN="$(cat /run/han-chat/secrets/MESSAGE_SAFETY_SERVICE_TOKEN)"
MESSAGE_ID="$(uuidgen)"
curl --silent --show-error --write-out '\nHTTP %{http_code}\n' --config - <<EOF
url = "https://processing.internal:8443/internal/safety/v2/messages/check"
cacert = "/opt/han-chat/backend/secrets/tls/processing-internal-ca.crt"
request = "POST"
header = "X-Service-Token: ${SAFETY_TOKEN}"
header = "Content-Type: application/json"
data = "{\"message_id\":\"${MESSAGE_ID}\",\"content_kind\":\"text\",\"text\":\"VM2 safety canary\",\"attachment\":null}"
EOF
unset SAFETY_TOKEN MESSAGE_ID

В standard mode ожидается HTTP 200, verdict=allow и непустые config_version/rules_version. Проверку 202 → Location → task GET выполняйте отдельным file smoke только с реальным versioned quarantine object: выдуманные S3 key/version/ETag не являются валидным тестом.

Для Bitrix status прочитайте token из уже защищённого secret file VM1 в переменную и передайте curl config через stdin, чтобы значение не попало в argv/history:

BITRIX_TOKEN="$(cat /run/han-chat/secrets/BITRIX_SYNC_SERVICE_TOKEN)"
curl --silent --show-error --output /tmp/vm2-sync-status.json \
  --write-out '%{http_code}\n' --config - <<EOF
url = "https://processing.internal:8443/internal/sync/v1/status"
cacert = "/opt/han-chat/backend/secrets/tls/processing-internal-ca.crt"
header = "Authorization: Bearer ${BITRIX_TOKEN}"
EOF
unset BITRIX_TOKEN
cat /tmp/vm2-sync-status.json
rm -f /tmp/vm2-sync-status.json

При BITRIX_SYNC_ENABLED=false ожидается закрытая/неготовая синхронизация, а не ложный успешный full-mode status. С машины вне VM1_PRIVATE_CIDRS и необязательных приватных/VPN-сетей OPS_CIDRS подключение к 8443 должно завершиться timeout/reject.

Gate 9 — canary на отсутствие секретов в логах и traces

Создайте фейковый, не production token marker и отправьте его с разрешённого тестового source IP:

CANARY можно ставить любой

CANARY="HAN_VM2_REDACTION_260813"
curl -sS -o /dev/null \
  -H "Authorization: Bearer ${CANARY}" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "auth[application_token]=${CANARY}" \
  "https://service4chat.han0107.ru/bitrix/sync/webhook/contact?token=${CANARY}"

На VM2 под root:

CANARY='HAN_VM2_REDACTION_260813'
if /usr/local/sbin/han-vm2-compose logs --no-color \
  nginx bitrix-sync message-safety-api otel-collector |
  grep -F -- "$CANARY"; then
  echo 'FAIL: canary попал в логи' >&2
  exit 1
fi
unset CANARY

В SigNoz выполните поиск этого же marker по logs и span attributes за окно теста: результат должен быть пустым. Отдельными фейковыми markers повторите проверку для DSN-подобной строки, S3 key и object key. Реальные secrets для такой проверки не используйте.

Не открывайте webhook-трафик, пока bitrix-sync отключён. Отключённый или упавший receiver должен возвращать retryable 503/закрытую маршрутизацию, никогда успешный 2xx ignored.

Политика отказов

  • Отказ зависимости Safety — fail-closed: VM1 не должна отправлять/продвигать контент.
  • Устаревшие/недоступные сигнатуры ClamAV отключают только файловую capability; ошибка сканирования никогда не превращается в allow.
  • Потеря Redis может убрать ускорение, но PostgreSQL остаётся источником истины.
  • Сбой OTEL ставит в очередь в пределах ограниченного тома и не должен менять вердикты.
  • Rollback не понижает схемы, не удаляет durable tasks/mappings и не запускает docker compose down -v.

Аварийный MOCK

Разрешены только эти пять sudo-команд:

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 — нет.

Проверка отправки в Сигноз

docker network ls | grep observability

docker run --rm --network han-processing_observability
ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest
traces --otlp-endpoint otel-collector:4317 --otlp-insecure
--service han-processing-otlp-smoke --traces 100 --rate 20

Проверка /usr/local/sbin/han-vm2-compose logs --since=5m otel-collector | grep -Ei 'error|refused|queue is full|Unauthenticated|tls' || echo 'нет ошибок экспорта'

после успешной проверки нужно удалить docker rmi ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

Проверка финальная

  1. Проверить автозапуск:
systemctl is-enabled \
  docker.service \
  han-chat-vm2-docker-firewall.service \
  han-secrets-vm2.service \
  han-processing.service \
  certbot.timer

(все enabled)

systemctl is-active \
  docker.service \
  han-chat-vm2-docker-firewall.service \
  han-processing.service \
  certbot.timer

(все active)

/usr/local/sbin/han-vm2-compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Ports}}"
  1. Провести reboot-gate. Только после проверки отдельного входа admin и доступа к консоли Selectel:
systemctl reboot

После переподключения повторить команды выше и кратко Gate 68: HTTPS, firewall, private Safety API.

  1. Зафиксировать итог релиза:
/usr/local/sbin/han-vm2-compose config --images
/usr/local/sbin/han-vm2-compose ps
systemctl list-timers certbot.timer
journalctl --no-pager -u han-processing.service -u han-secrets-vm2.service

Сохранить версии образов, дату приёмки и результаты gates без значений секретов.

  1. Настроить эксплуатационный мониторинг:
  • container unhealthy/restart/OOM;
  • срок TLS;
  • возраст ClamAV signatures;
  • OTEL queue/export errors;
  • disk/RAM;
  • активный MOCK mode;
  • недоступность private Safety API.
  1. Далее — отдельный controlled cutover Message Safety на VM1: private URL, internal CA, service token, API integration, rollback rehearsal и функциональные проверки.

  2. bitrix-sync пока оставить:

BITRIX_SYNC_ENABLED=false
BITRIX_SYNC_MODE=disabled

Public allow-list — только deny all;. Включать Bitrix можно лишь после выполнения gates module-07: поля портала, webhooks, migrations, grants, backfill/watermark и rollback rehearsal.

Таким образом, ближайший шаг сейчас — reboot-gate и фиксация приёмки VM2. Затем переход к интеграции VM1, а не немедленное включение Bitrix.