All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI / backend-tests (pull_request) Successful in 17m38s
Сегодня подлинность хоста не проверяется нигде: deploy-tradein.yml явно ставил StrictHostKeyChecking=no, а во всех пяти вызовах appleboy/ssh-action не задан fingerprint. Пока Forgejo, раннеры и прод жили на одной машине, это было почти бесплатно. После переезда 30.08 тот же канал идёт через интернет и уносит GHCR_PAT, OPENAI_API_KEY, OBJECTIVE_API_KEY, GLITCHTIP_BACKEND_DSN и сам DEPLOY_SSH_KEY. Вызовов оказалось пять, а не четыре: deploy-obsidian.yml тоже ходит на DEPLOY_HOST с DEPLOY_SSH_KEY. Отпечаток добавлен во все. Механизм инертен по конструкции самого действия, а не нашей ветвлёнкой: appleboy/ssh-action@v1.0.3 -> drone-ssh 1.7.3 -> easyssh-proxy v1.5.0, где hostKeyCallback = ssh.InsecureIgnoreHostKey() и сверка включается только при непустом Fingerprint. Незаданный секрет = сегодняшнее поведение бит в бит. Но инертность, безопасная для мержа, опасна для самого окна: забытый секрет означал бы зелёный деплой через интернет без всякой проверки. Поэтому перед каждым ssh-шагом стоит преамбула, которая при пустом секрете печатает ::warning с перечислением того, что именно едет по этому каналу. Warning, а не отказ: секрета сегодня нет ни у кого, отказ сломал бы деплой в момент мержа. Ручной ssh в "Resolve deployed base SHA" отпечаток принять не может, ему отдан отдельный секрет с known_hosts по той же схеме. Заодно расхождение ключа перестало быть неотличимым от недоступного хоста: раньше 2>/dev/null глотал "Host key verification failed" и печаталось обычное сообщение про фолбэк на полную пересборку. Свойство fail-safe сохранено - при любой ошибке RAW_SHA пустеет и включается build-all. ops/selectel-ci-access.sh - аддитивная выдача CI-доступа на Selectel по решению владельца "сейчас только добавляем, удаляем после переезда": ставит отдельный ключ робота с from="адрес раннера", добавляет узкое правило ufw, не трогая общее, и печатает план сужения, который сам не выполняет. Ни одной команды, способной отрезать текущую сессию: sshd не перезапускается, default policy ufw не трогается, в authorized_keys только дозапись. Отзыв старого ключа, сужение правил и уборка секретов - в #3075, после подтверждённого переезда. Refs #3029, #3075, #3057, #2989
150 lines
11 KiB
YAML
150 lines
11 KiB
YAML
name: Deploy Infra Host
|
||
|
||
# Синхронизация /opt/gendesign на ХОСТЕ, КОТОРЫЙ ОСТАЁТСЯ (#3059, #3057).
|
||
#
|
||
# ЗАЧЕМ. Сегодня Beget — и прод, и инфраструктура одновременно, поэтому его
|
||
# рабочее дерево обновляет обычный `deploy.yml` (шаг `git reset --hard
|
||
# origin/main` по SSH на `secrets.DEPLOY_HOST`). После переезда 30.08
|
||
# `DEPLOY_HOST` станет указывать на Selectel — и /opt/gendesign на Beget
|
||
# перестанет обновляться СОВСЕМ. Молча.
|
||
#
|
||
# А из этого каталога на Beget продолжат работать:
|
||
# - cron-скрипты бэкапов: ops/backup.sh, ops/backup-forgejo.sh,
|
||
# ops/check-backup-staleness.sh, ops/lib-backup.sh, ops/docker-prune.sh
|
||
# - docker-compose.prod.yml для Forgejo / GlitchTip / CouchDB
|
||
# - Caddyfile + caddy/sites/infra.caddy — единственный публичный вход для
|
||
# git.gendsgn.ru, errors.gendsgn.ru, obsidian.gendsgn.ru (#3062)
|
||
#
|
||
# То есть любая будущая правка этих файлов легла бы в main и никогда не доехала
|
||
# до машины, которая их исполняет. Это ровно класс #2887 («скрипт запускается по
|
||
# cron из /opt/gendesign, куда попадает только через git reset --hard шага
|
||
# деплоя»), но не на уровне одного файла, а на уровне целого хоста.
|
||
#
|
||
# ПОЧЕМУ ОТДЕЛЬНЫЙ WORKFLOW, А НЕ JOB В deploy.yml. Разные адресаты и разные
|
||
# вердикты: «выкатили приложение на Selectel» и «синхронизировали инфраструктуру
|
||
# на Beget» — два независимых факта, и падение второго не должно читаться как
|
||
# неудавшийся деплой продукта. Плюс триггеры разные: инфра-хосту не нужны
|
||
# пересборки backend/frontend.
|
||
#
|
||
# ИНЕРТЕН, ПОКА НЕ ЗАДАН INFRA_DEPLOY_HOST. Сейчас, до переезда, Beget и есть
|
||
# DEPLOY_HOST — второй проход по тому же хосту был бы лишним и мог бы состязаться
|
||
# с основным деплоем за докер-демон (#2950). Поэтому job не делает ничего, пока
|
||
# секрет пуст: включается ОДНОЙ настройкой в момент, когда хосты разъедутся.
|
||
|
||
# ── ПОДЛИННОСТЬ ХОСТА (#3029) ────────────────────────────────────────────────
|
||
# Переезд 30.08 (#3057) уводит цель деплоя на Selectel, а раннеры оставляет на
|
||
# Beget — SSH становится междоузловым, через интернет. Поэтому у вызова
|
||
# appleboy/ssh-action ниже появился вход `fingerprint`.
|
||
# ЧТО ЗАДАТЬ: секрет INFRA_DEPLOY_SSH_FINGERPRINT =
|
||
# ssh-keyscan -t ecdsa -p <порт> <хост> | ssh-keygen -lf - | awk '{print $2}'
|
||
# (значение с префиксом `SHA256:`; именно ecdsa — см. разбор в deploy.yml).
|
||
# ПОБАЙТОВО: значение сравнивается как есть, без trim — лишний пробел/перевод
|
||
# строки при копипасте включает проверку и роняет ssh-шаг с `host key
|
||
# fingerprint mismatch`.
|
||
# ПОКА СЕКРЕТ НЕ ЗАДАН — поведение прежнее: пустой fingerprint у easyssh-proxy
|
||
# v1.5.0 означает ssh.InsecureIgnoreHostKey(), то есть ровно как до этого PR.
|
||
# Включается одной настройкой, как INFRA_DEPLOY_HOST (#3059) и fail-open у
|
||
# TRADEIN_INTERNAL_AUTH_SECRET (#2989).
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
# Ровно то, что исполняется НА ОСТАЮЩЕМСЯ хосте. Намеренно НЕ включены
|
||
# backend/** и frontend/** — их образы туда не едут.
|
||
- "ops/*.sh"
|
||
- "Caddyfile"
|
||
- "caddy/**"
|
||
- "docker-compose.prod.yml"
|
||
- "docker-compose.obsidian.yml"
|
||
- ".forgejo/workflows/deploy-infra.yml"
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
sync-infra-host:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Проверить, разъехались ли хосты
|
||
id: gate
|
||
env:
|
||
INFRA_HOST: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -z "${INFRA_HOST:-}" ]; then
|
||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||
echo "INFRA_DEPLOY_HOST не задан — хосты ещё не разъехались."
|
||
echo "Инфраструктуру обновляет обычный deploy.yml. Ничего не делаю."
|
||
else
|
||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||
echo "INFRA_DEPLOY_HOST задан — синхронизирую остающийся хост."
|
||
fi
|
||
|
||
# #3029: ВИДИМОСТЬ, А НЕ БЛОКИРОВКА. Отсутствие проверки хоста обязано быть
|
||
# громким: easyssh-proxy v1.5.0 при пустом fingerprint молча оставляет
|
||
# ssh.InsecureIgnoreHostKey(), и незащищённый деплой выглядит ровно как
|
||
# защищённый — зелёным. Шаг намеренно НЕ падает: секрета сегодня нет ни у
|
||
# кого, отказ сломал бы деплой в момент мержа этого PR, а правило здесь —
|
||
# «инертно по умолчанию, включается одной настройкой». Заведут секрет —
|
||
# предупреждение исчезнет само.
|
||
- name: Подлинность хоста — статус проверки (#3029)
|
||
if: steps.gate.outputs.enabled == 'true'
|
||
env:
|
||
HOST_FINGERPRINT: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${HOST_FINGERPRINT:-}" ]; then
|
||
echo "Подлинность хоста: сверяется по INFRA_DEPLOY_SSH_FINGERPRINT."
|
||
else
|
||
echo '::warning title=SSH без проверки подлинности хоста::INFRA_DEPLOY_SSH_FINGERPRINT не задан — ключ остающегося хоста НЕ проверяется (#3029). Фолбэка на DEPLOY_SSH_FINGERPRINT здесь нет и быть не должно: это другая машина. По каналу едет INFRA_DEPLOY_SSH_KEY и выполняется git reset на /opt/gendesign. Как снять отпечаток — см. шапку этого файла.'
|
||
echo '###############################################################'
|
||
echo '# ВНИМАНИЕ (#3029): INFRA_DEPLOY_SSH_FINGERPRINT не задан.'
|
||
echo '# Ключ хоста НЕ проверяется — канал уязвим к MITM.'
|
||
echo '# Как снять отпечаток — см. шапку этого файла.'
|
||
echo '###############################################################'
|
||
fi
|
||
|
||
- name: Синхронизировать /opt/gendesign на остающемся хосте
|
||
if: steps.gate.outputs.enabled == 'true'
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
host: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
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 }}
|
||
# #3029: БЕЗ фолбэка на DEPLOY_SSH_FINGERPRINT — в отличие от user/key/port
|
||
# выше. Те у двух хостов совпадают, а отпечаток — это идентичность
|
||
# КОНКРЕТНОЙ машины: после переезда INFRA_DEPLOY_HOST=Beget, а
|
||
# DEPLOY_HOST=Selectel, и фолбэк означал бы сверку ключа Beget'а с
|
||
# отпечатком Selectel'а — гарантированный отказ ровно у того workflow,
|
||
# который чинит остающийся хост. Пусто → проверка пропускается.
|
||
fingerprint: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
script: |
|
||
set -euo pipefail
|
||
cd /opt/gendesign
|
||
git fetch origin main
|
||
git reset --hard origin/main
|
||
|
||
# Тот же chmod, что и в deploy.yml: cron зовёт скрипты через `bash`,
|
||
# но restore-drill и ручные запуски рассчитывают на +x.
|
||
chmod +x ops/*.sh 2>/dev/null || true
|
||
|
||
# Caddy здесь несёт ТОЛЬКО остающиеся домены (CADDY_SITES=infra).
|
||
# reload, а не recreate: конфиг примонтирован read-only, контейнер
|
||
# читает тот же файл, который только что обновил git. Если конфиг
|
||
# невалиден — reload откажет ГРОМКО, а старый останется работать,
|
||
# то есть падение здесь не роняет git/errors/obsidian.
|
||
if docker ps --format '{{.Names}}' | grep -q '^gendesign-caddy-1$'; then
|
||
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
|
||
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||
echo "✓ конфиг прокси перезагружен"
|
||
else
|
||
echo "⚠ контейнер caddy не найден — пропускаю reload"
|
||
fi
|
||
|
||
# НАМЕРЕННО НЕ ДЕЛАЕТСЯ:
|
||
# - docker image prune: конкурирует с деплоем продукта за leases
|
||
# докер-демона (#2950). Прун на этом хосте остаётся за
|
||
# еженедельным ops/docker-prune.sh.
|
||
# - перезапуск Forgejo / GlitchTip / CouchDB: правка ops-скрипта
|
||
# не повод ронять git. Их обновление — осознанное действие.
|
||
echo "✓ рабочее дерево синхронизировано: $(git rev-parse --short HEAD)"
|