fix(ci): стек CouchDB после переезда остаётся на своём хосте
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped

Волт 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
This commit is contained in:
bot-backend 2026-08-24 20:57:25 +03:00
parent 2feb0de446
commit a77bfa5270

View file

@ -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