From a77bfa5270726ec24f95c6df58f0045d5f4e3d6c Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 24 Aug 2026 20:57:25 +0300 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=D1=81=D1=82=D0=B5=D0=BA=20CouchDB?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B5=D0=B7=D0=B4=D0=B0=20=D0=BE=D1=81=D1=82=D0=B0=D1=91=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BD=D0=B0=20=D1=81=D0=B2=D0=BE=D1=91=D0=BC=20?= =?UTF-8?q?=D1=85=D0=BE=D1=81=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Волт Obsidian живёт на Beget вместе с Forgejo и GlitchTip и там же остаётся. А deploy-obsidian.yml ходил на secrets.DEPLOY_HOST безусловно — тот самый секрет, который 30.08 будет переставлен на Selectel. Значит в день переезда этот workflow молча сменил бы адресата: git reset на /opt/gendesign продуктового хоста и docker compose стека CouchDB там же, а волт на Beget тем временем перестал бы обновляться. Без единой ошибки — деплой зелёный, машина другая. Файл сам это признавал в шапке: проблема была описана и намеренно оставлена, потому что прошлая правка касалась подлинности хоста, а не маршрутизации. Теперь адресат берётся как INFRA_DEPLOY_HOST с фолбэком на DEPLOY_HOST. До разъезда это одна машина, поэтому сегодня не меняется ничего; после — workflow остаётся на инфраструктурном хосте без правки файла в горячий момент. Схема и фолбэки user/key/port скопированы с deploy-infra.yml, чтобы два файла про один и тот же хост не расходились в поведении. Отпечаток выбирается В ПАРЕ с адресатом, отдельным шагом с явным if. Соблазн написать `secrets.INFRA_FP || secrets.DEPLOY_FP` — ловушка: если инфра-хост задан, а его отпечаток нет, выражение свалится на отпечаток другой машины, и ssh откажет со сверкой ключа Beget'а против отпечатка Selectel'а. Перекрёстного фолбэка здесь нет и быть не должно — ровно как в deploy-infra.yml. Отпечаток идёт вниз через output шага: это публичный хеш ключа хоста, не секрет. Приватный ключ так передавать нельзя, он остался прямой ссылкой на секрет. $GITHUB_OUTPUT — построчный формат «ключ=значение», поэтому перевод строки внутри значения означает инъекцию произвольного output'а. Отпечаток однострочный по определению, но в поле секрета лишний перевод строки попадает копипастой на раз — шапка файла об этом предупреждает отдельно. Проверка падает с внятным текстом вместо тихой подмены: молча вычищать нельзя, сверка побайтовая и trim изменил бы результат. Проверено: YAML парсится, шаг проходит bash -n, поведение прогнано на трёх значениях — нормальный отпечаток даёт rc=0 и строку про сверку, пустой даёт предупреждение и не роняет деплой (как сегодня), значение с переводом строки отбивается с rc=1. Refs #3062, #3029, #3057 --- .forgejo/workflows/deploy-obsidian.yml | 83 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/.forgejo/workflows/deploy-obsidian.yml b/.forgejo/workflows/deploy-obsidian.yml index 9dfdbd2f..cbba26d2 100644 --- a/.forgejo/workflows/deploy-obsidian.yml +++ b/.forgejo/workflows/deploy-obsidian.yml @@ -32,13 +32,21 @@ name: Deploy Obsidian # v1.5.0 означает ssh.InsecureIgnoreHostKey(), то есть ровно как до этого PR. # Включается одной настройкой, как INFRA_DEPLOY_HOST (#3059) и fail-open у # TRADEIN_INTERNAL_AUTH_SECRET (#2989). -# NB: этот workflow ходит на secrets.DEPLOY_HOST, поэтому и отпечаток берётся -# DEPLOY_SSH_FINGERPRINT — пара «хост+отпечаток» обязана меняться вместе. Но -# CouchDB по плану ОСТАЁТСЯ на Beget вместе с Forgejo/GlitchTip (#3062), а -# DEPLOY_HOST после 30.08 будет указывать на Selectel. Значит у этого файла есть -# отдельная, ещё не закрытая проблема адресата — тот же класс, что #3059. Здесь -# намеренно НЕ трогаем: правка про подлинность хоста, а не про маршрутизацию. -# При переводе на INFRA_DEPLOY_HOST не забыть переключить и отпечаток. +# АДРЕСАТ (#3062). CouchDB/Obsidian ОСТАЁТСЯ на Beget вместе с Forgejo и +# GlitchTip, а DEPLOY_HOST после 30.08 будет указывать на Selectel. Раньше этот +# workflow ходил на DEPLOY_HOST безусловно — то есть в день переезда молча начал +# бы разворачивать стек CouchDB не на той машине: git reset на /opt/gendesign +# продуктового хоста, а волт на Beget тем временем перестал бы обновляться. +# Отказа при этом не было бы — деплой зелёный, адресат другой. +# +# Теперь адресат берётся как INFRA_DEPLOY_HOST, а если он не задан — DEPLOY_HOST. +# До переезда это одна и та же машина, поэтому поведение не меняется; после — +# workflow сам остаётся на инфраструктурном хосте, без правки этого файла. +# +# Отпечаток идёт В ПАРЕ с адресатом и БЕЗ перекрёстного фолбэка: сверять ключ +# Beget'а с отпечатком Selectel'а — гарантированный отказ. Задан INFRA_DEPLOY_HOST +# → берётся INFRA_DEPLOY_SSH_FINGERPRINT; не задан → DEPLOY_SSH_FINGERPRINT. +# Пусто в выбранной ветке → проверка подлинности пропускается, как и раньше. # ───────────────────────────────────────────────────────────────────────────── on: push: @@ -68,17 +76,46 @@ jobs: # кого, отказ сломал бы деплой в момент мержа этого PR, а правило здесь — # «инертно по умолчанию, включается одной настройкой». Заведут секрет — # предупреждение исчезнет само. - - name: Подлинность хоста — статус проверки (#3029) + - name: Адресат и подлинность хоста (#3062, #3029) + id: target env: - HOST_FINGERPRINT: ${{ secrets.DEPLOY_SSH_FINGERPRINT }} + INFRA_HOST: ${{ secrets.INFRA_DEPLOY_HOST }} + INFRA_FINGERPRINT: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }} + MAIN_FINGERPRINT: ${{ secrets.DEPLOY_SSH_FINGERPRINT }} run: | set -euo pipefail - if [ -n "${HOST_FINGERPRINT:-}" ]; then - echo "Подлинность хоста: сверяется по DEPLOY_SSH_FINGERPRINT." + # Отпечаток — публичный хеш ключа хоста, не секрет: его можно + # передать через output. Приватный ключ так передавать нельзя, + # поэтому он остаётся прямой ссылкой на секрет в шаге ниже. + if [ -n "${INFRA_HOST:-}" ]; then + echo "Адресат: INFRA_DEPLOY_HOST — хосты разъехались, стек CouchDB едет на инфраструктурный хост." + HOST_FINGERPRINT="${INFRA_FINGERPRINT:-}" + FINGERPRINT_SOURCE="INFRA_DEPLOY_SSH_FINGERPRINT" else - echo '::warning title=SSH без проверки подлинности хоста::DEPLOY_SSH_FINGERPRINT не задан — ключ хоста НЕ проверяется (#3029). По каналу едет DEPLOY_SSH_KEY и разворачивается стек CouchDB/Obsidian. После переезда на Selectel (#3057) соединение идёт через интернет. Как снять отпечаток — см. шапку этого файла.' + echo "Адресат: DEPLOY_HOST — INFRA_DEPLOY_HOST не задан, хосты ещё одна машина." + HOST_FINGERPRINT="${MAIN_FINGERPRINT:-}" + FINGERPRINT_SOURCE="DEPLOY_SSH_FINGERPRINT" + fi + # $GITHUB_OUTPUT — формат «ключ=значение» построчно, поэтому перевод + # строки внутри значения означает инъекцию произвольного output'а. + # Отпечаток однострочный по определению (SHA256:...), а вот копипаста + # в поле секрета лишний \n добавляет легко — шапка этого файла об этом + # прямо предупреждает. Не вычищаем молча: сверка побайтовая, тихий trim + # изменил бы результат проверки. Падаем с внятным текстом. + case "${HOST_FINGERPRINT}" in + *[![:print:]]*) + echo "ОШИБКА: ${FINGERPRINT_SOURCE} содержит перевод строки или непечатный символ." >&2 + echo "ОШИБКА: значение должно быть одной строкой вида SHA256:xxxx — перезадай секрет без лишних символов." >&2 + exit 1 + ;; + esac + echo "fingerprint=${HOST_FINGERPRINT}" >> "$GITHUB_OUTPUT" + if [ -n "${HOST_FINGERPRINT:-}" ]; then + echo "Подлинность хоста: сверяется по ${FINGERPRINT_SOURCE}." + else + echo "::warning title=SSH без проверки подлинности хоста::${FINGERPRINT_SOURCE} не задан — ключ хоста НЕ проверяется (#3029). По каналу едет ssh-ключ и разворачивается стек CouchDB/Obsidian. После разъезда хостов (#3057) соединение идёт через интернет. Как снять отпечаток — см. шапку этого файла." echo '###############################################################' - echo '# ВНИМАНИЕ (#3029): DEPLOY_SSH_FINGERPRINT не задан.' + echo "# ВНИМАНИЕ (#3029): ${FINGERPRINT_SOURCE} не задан." echo '# Ключ хоста НЕ проверяется — канал уязвим к MITM.' echo '# Как снять отпечаток — см. шапку этого файла.' echo '###############################################################' @@ -87,13 +124,19 @@ jobs: - name: Deploy obsidian stack via SSH uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - port: ${{ secrets.DEPLOY_PORT || 22 }} - # #3029: подлинность хоста. Секрет НЕ задан → пустая строка → easyssh-proxy - # оставляет ssh.InsecureIgnoreHostKey(), то есть сегодняшнее поведение. - fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }} + # #3062: адресат — инфраструктурный хост, если хосты уже разъехались. + # До этого INFRA_DEPLOY_HOST пуст и всё идёт на DEPLOY_HOST, как раньше. + host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }} + # user/key/port с фолбэком: у двух хостов они совпадают, а отдельные + # INFRA_*-секреты может и не завести — тогда работают общие. + username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }} + key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.INFRA_DEPLOY_PORT || secrets.DEPLOY_PORT || 22 }} + # #3029: подлинность хоста. Отпечаток выбран шагом выше В ПАРЕ с + # адресатом — перекрёстного фолбэка здесь быть не должно, иначе после + # переезда ключ Beget'а сверялся бы с отпечатком Selectel'а. + # Пусто → easyssh-proxy оставляет ssh.InsecureIgnoreHostKey(), как сегодня. + fingerprint: ${{ steps.target.outputs.fingerprint }} script: | set -euo pipefail cd /opt/gendesign