# 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` скрипт отклоняет: ```sh 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: ```sh 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`: ```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' \ HARDEN_SSH=true \ SKIP_APT_UPGRADE=true \ /root/setup-vm2.sh ``` Это добавит `PermitRootLogin no` и `AllowUsers deploy admin`. Публичные bootstrap-копии после проверки можно удалить под `admin`: ```sh 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`: ```powershell $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. Значение должно совпасть с локальным: ```sh 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 или лишний проект: ```sh 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 из активного релиза. Приложение всё ещё не запускается: ```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' \ 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`: ```sh 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): ```bash 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 ```bash 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 ``` Проверка: ```bash 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_KEY` ← `vm2-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: ```sh 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 должен оставаться закрытым: ```sh 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). Пример: ```nginx # 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 (или прокси), например: ```nginx allow 185.x.x.x/32; deny all; ``` ### Где править На VM2 после раскладки релиза: ```text /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 ```sh 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: ```sh 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 не использует: ```sh 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-сертификат: ```sh 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: ```sh 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: ```sql 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 '' /usr/local/sbin/han-vm2-compose --profile ops run --rm \ --entrypoint message-safety-config message-safety-migrate \ activate --version 1 --approved-by '' ``` Потом права отзываем 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: ```sh 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: ```sh 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`. После них: ```sh /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 без вывода их содержимого: ```sh /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: ```sh /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: ```sh 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: ```sh 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: ```sh openssl s_client -connect service4chat.han0107.ru:443 \ -servername service4chat.han0107.ru \ -verify_hostname service4chat.han0107.ru -verify_return_error &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-команд: ```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` — нет. Проверка отправки в Сигноз 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. Проверить автозапуск: ```sh 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}}" ``` 2. Провести reboot-gate. Только после проверки отдельного входа `admin` и доступа к консоли Selectel: ```sh systemctl reboot ``` После переподключения повторить команды выше и кратко Gate 6–8: HTTPS, firewall, private Safety API. 3. Зафиксировать итог релиза: ```sh /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 без значений секретов. 4. Настроить эксплуатационный мониторинг: - container unhealthy/restart/OOM; - срок TLS; - возраст ClamAV signatures; - OTEL queue/export errors; - disk/RAM; - активный MOCK mode; - недоступность private Safety API. 5. Далее — отдельный controlled cutover Message Safety на VM1: private URL, internal CA, service token, API integration, rollback rehearsal и функциональные проверки. 6. `bitrix-sync` пока оставить: ```dotenv 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.