Files
HY2XS_flamy/docs/13-production-runbook.md
T
founder 76d78ac71f fix(orchestrator): закрыть два остатка на стыке guard и замка операций
Оба дефекта — в механизмах, введённых предыдущими коммитами, и оба относятся к
гарантиям, ради которых эти механизмы вводились.

1. Отказ записи `auto-rollback-fired` оставался незамеченным.

Инвариант фиксации "маркера нет и юниты inactive => guard не сработал" верен
только при дополнительном условии "guard способен записать маркер". Пока `rc=0`
стояло ПОСЛЕ создания маркера, отказ записи (заполненный tmpfs /run, read-only
ФС) не влиял ни на что: скрипт успешно восстанавливал прежний firewall,
завершался кодом 0, юнит уходил в inactive, маркера не было — и операция
фиксировала успех после реально сработавшего отката.

`rc` объявляется до первой операции, включая создание маркера, а ранний выход
возвращает его вместо жёсткого `exit 0`. У факта срабатывания появилось два
независимых канала: маркер и отказ юнита, потому что на пути фиксации успеха
допустим ровно один ActiveState — inactive.

Заодно маркер создаётся `touch`, а не `: >file`: двоеточие — special builtin
POSIX, ошибка перенаправления на нём обязана завершить неинтерактивный shell
целиком, и в dash скрипт умер бы ДО восстановления firewall.

2. Новая операция могла начаться, пока guard предыдущей ещё вооружён.

Замок действует, пока жив процесс-держатель. Guard — отдельный объект systemd,
переживающий свой процесс:

    A берёт замок -> применяет firewall -> вооружает guard на 45s
    A аварийно умирает
    B берёт замок и начинает менять production paths
    guard A срабатывает и возвращает firewall, который был ДО A

Случай SIGTERM/SIGHUP хуже, чем kill -9: обработчик снимает замок сам, поэтому
проверка живости держателя не видит вообще ничего, а таймер остаётся.

Введён барьер покоя `assertNoPendingRollbackGuard`, через который проходит
каждый захват замка — дважды, до и после, потому что между ними умирающая
операция успевает вооружить guard, — и PHASE 0 установщика. Непокоем считаются
active/activating/deactivating/reloading; `failed` и `inactive` — покой, иначе
барьер блокировал бы `repair`, которым чинят последствия.

Плюс P1: восстановление UnitFileState у nftables.service больше не обещает
точности, которой не даёт. `enable --runtime` не удаляет постоянную ссылку,
поэтому "восстановление" enabled-runtime оставляло юнит включённым в обоих
scope. Восстанавливаются enabled/disabled — то, что операция реально меняет, —
остальные состояния называются оператору и не трогаются.

Тесты: поведенческая проверка раннего пути rollback-скрипта настоящим shell
(ветка заканчивается до первой команды восстановления и безопасна для запуска),
проверка двойного вызова барьера и снятия замка при его отказе, структурные
инварианты. Приёмка и docs (D1h, уточнение D1f) — там же.
2026-08-31 03:30:14 +05:00

18 KiB
Raw Blame History

HY2XS production runbook

1. Supported target

  • clean Debian 13 amd64
  • single host install profile
  • IPv4-only runtime model

2. Required prerequisites

apt-get update
apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables systemd

sudo обязателен для permission smoke-checks от имени runtime-пользователей.

Предварительная ручная установка sudo до запуска ./install.sh не требуется: на clean-host install-flow ставит его на стадии deps до выполнения smoke-checks.

3. Required open ports

  • UDP ${HY2XS_HYSTERIA_PORT}
  • TCP ${HY2XS_UI_PORT} (обычно localhost bind)
  • TCP ${HY2XS_SSH_PORT}
  • TCP 80/443 для ACME (в зависимости от типа challenge)

4. Clean host assumptions

  • нет legacy-конфликта по runtime-users (hysteria, hy2xs-admin)
  • нет конфликтующего не-HY2XS nftables entrypoint
  • install запускается от root

5. Install command

./install.sh --non-interactive

Важно: packaged baseline использует HY2XS_SSH_PORT=2323 по умолчанию. На target-хосте оператор обязан выставить свой рабочий SSH-порт в /etc/hy2xs/hy2xs.env и применить изменения через reconfigure --apply.

6. Post-install verification

systemctl status hysteria-server
systemctl status hy2xs-admin
ss -H -lun | grep ':443'
ss -H -ltn | grep ':8080'
nft list ruleset
cat /var/lib/hy2xs/install-state.json

7. Permission verification

ls -l /etc/hy2xs/hy2xs.env
ls -l /etc/hysteria/config.yaml

sudo -u hysteria test -r /etc/hysteria/config.yaml
sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml
sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml
sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env

8. Firewall recovery

Если install/reconfigure падают после firewall apply:

  • rollback guard не должен отменяться до успешного smoke;
  • для recovery использовать вывод оркестратора и перезапускать apply только после устранения root-cause.

Откат после операционного отказа выполняется целиком и сам по себе не может быть отменён: ни неудачной записью состояния в /var/lib/hy2xs, ни отказом одной из своих стадий. Поэтому в журнале нужно читать две разные вещи:

Строка в журнале Что она означает
failed to persist failure state, continuing with the mandatory rollback маркер не обновился (обычно заполненный диск), но восстановление выполнено; после освобождения места запустить doctor
rollback stage "<имя>" failed, continuing with the remaining stages конкретная половина восстановления не отработала; остальные выполнены
rollback finished with N failed stage(s); manual recovery may be required итог: перечисленные стадии требуют ручной проверки
rollback completed: N stage(s) succeeded восстановление отработало полностью
manual recovery data preserved at /run/hy2xs/rollback/<op> firewall восстановлен не полностью; прежние nftables.conf и hy2xs.nft лежат по этому пути
firewall rollback guard armed: … fires in 45s guard взведён; с этого момента операция обязана снять его до фиксации успеха
firewall rollback guard disarmed and proven inactive guard снят, и это подтверждено состоянием юнитов и отсутствием маркера срабатывания
automatic firewall rollback has already fired guard успел сработать; сервер работает на прежнем firewall, операция обязана завершиться отказом
firewall rollback guard <unit> is still in state "…" остановить guard не удалось; фиксация успеха запрещена, разбирайтесь с systemd

Отдельно про сработавший guard. Окно 45 секунд намеренно короче худшего случая smoke и не обязано его покрывать: доказательством служит не время, а маркер

/run/hy2xs/rollback/<op-id>/auto-rollback-fired

Если он есть — операция откатывается независимо от результата smoke, и в маркере установки появляется phase: firewall_guard_fired. Это значит: сервер жив и работает на прежнем firewall, а причину, по которой проход не уложился в окно, надо искать в journal — обычно это медленный старт одного из сервисов.

Юнит автоотката при частичном восстановлении уходит в failed, поэтому его стоит прочитать целиком:

journalctl -u 'hy2xs-fw-rollback-*' --no-pager

Резервные копии не удаляются, пока восстановление не подтверждено, и переживают долговечную запись phase: installed. Поэтому при разборе неудачи всегда осмысленно посмотреть:

ls -la /run/hy2xs/rollback/          # копии firewall текущей операции
ls -la /etc/hy2xs/backups/           # копия состояния до последнего reconfigure
cat /etc/hy2xs/backups/*/manifest.json

Имя каталога совпадает с полем op_id в /var/lib/hy2xs/install-state.json.

Манифест прямо говорит, какие файлы существовали до операции, а какие нет: запись "present": false означает, что откат обязан был файл удалить, а не восстановить.

Наружу оркестратор всегда пробрасывает исходную ошибку операции, а не проблему внутри отката: последняя — это информация о том, что осталось не восстановленным, а не причина отказа.

8a. Одна операция за раз

install, reconfigure, repair и doctor сериализованы эксклюзивным замком:

/run/lock/hy2xs-orchestrator.lock

Вторая операция отказывает сразу и до первой мутации — до снятия резервной копии, до записи конфигов, до firewall:

another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)

doctor тоже берёт замок: диагностика в середине транзакции описывает промежуточное состояние сервера и выдаёт бессмысленные ошибки по временным несоответствиям.

status и diagnostics collect замок не берут — они нужны в том числе во время долгой операции, — но сообщают о ней:

hy2xs-orchestrator status --package-dir /usr/local/lib/hy2xs/package
# "operation_in_progress": "reconfigure (pid 4242, started at …)"

Замок снимается при любом завершении держателя: штатном, по Ctrl+C, по SIGTERM от systemd и при обрыве SSH. Если процесс был убит kill -9, следующая операция обнаружит мёртвого держателя и переиспользует замок сама:

operation lock … is held by install (pid 1234), which is no longer running; reclaiming it

Замка при этом недостаточно, и это важно. Он действует, пока жив процесс-держатель, а rollback guard firewall — отдельный объект systemd, переживающий свой процесс. Аварийно умершая операция оставляет guard вооружённым, и он способен вернуть прежний firewall уже посреди следующей операции. Поэтому каждый захват замка проходит ещё и через барьер покоя:

previous HY2XS operation is no longer running, but its firewall rollback guard
is still armed: hy2xs-fw-rollback-<op-id>.timer (active)

Условие старта — не «PID предыдущей мёртв», а «у предыдущей не осталось исполнителей, способных изменить систему». Ждать нужно не больше 45 секунд с момента применения firewall; failed у guard покою не мешает и означает, что пора смотреть journalctl -u 'hy2xs-fw-rollback-*' и запускать repair.

/run/lock — это tmpfs, поэтому перезагрузка снимает замок в любом случае. Удалять файл руками нужно только если в нём оказалось непонятное содержимое: такой замок сознательно не переиспользуется автоматически — непонятый файл не доказывает, что операции нет.

9. Reconfigure flow

hy2xs-orchestrator reconfigure --package-dir /usr/local/lib/hy2xs/package --config /etc/hy2xs/hy2xs.env --dry-run
hy2xs-orchestrator reconfigure --package-dir /usr/local/lib/hy2xs/package --config /etc/hy2xs/hy2xs.env --apply

10. Admin bootstrap credentials

  • HY2XS_ADMIN_INITIAL_PASSWORD и HY2XS_ADMIN_CON_PASS — install-only bootstrap поля.
  • изменение значений в /etc/hy2xs/hy2xs.env после install не выполняет автоматическую ротацию существующих credentials.

11. IPv4/IPv6 policy

  • HY2XS работает в IPv4-only режиме.
  • если IPv6 включён на хосте/провайдере — это вне baseline и должно быть отдельно управляемо оператором.
  • HY2XS_DNS_AAAA_POLICY управляет реакцией preflight на DNS AAAA:
    • strict (default) — install/reconfigure прекращается при наличии AAAA;
    • warn — выводится warning и выполнение продолжается;
    • off — AAAA-проверка игнорируется.

11a. Публичный endpoint

  • preflight проверяет, что A-записи HY2XS_PUBLIC_HOSTHY2XS_DOMAIN, если он отличается) ведут на публичные IPv4 этого сервера;
  • проверка работает в install, reconfigure и doctor;
  • адрес сервера определяется локально по интерфейсам, без обращения к внешним сервисам определения IP;
  • HY2XS_PUBLIC_ENDPOINT_POLICY управляет строгостью:
    • strict (default) — расхождение останавливает операцию;
    • warn — warning и продолжение (NAT, floating IP, anycast);
    • off — сравнение не выполняется;
  • отсутствие A-записи остаётся фатальным при любом значении политики.

Типичный сценарий, ради которого это сделано: провайдер принудительно сменил IPv4, DNS остался старым. До v1 doctor в такой ситуации отвечал успехом, а клиентская ссылка отправляла людей на чужую машину.

12. Validation command

hy2xs-orchestrator doctor --package-dir /usr/local/lib/hy2xs/package --config /etc/hy2xs/hy2xs.env

Команда выполняет preflight + smoke как post-install/post-reboot validation.

doctor не перезапускает сервисы: он диагностирует работающую установку. Раньше он собирал контекст с параметрами по умолчанию и звал общий smoke, а тот первым же действием выполняет systemctl restart hysteria-server hy2xs-admin — то есть команда, которую этот раздел предлагает запускать при подозрении на проблему, гарантированно обрывала все живые VPN-соединения, включая случай, когда с сервисом всё в порядке. Диагностика, меняющая то, что диагностирует, отвечает не на заданный вопрос: после рестарта проверяется уже другое состояние.

Остальные проверки smoke выполняются полностью — слушатели, права и владельцы файлов, machine auth (включая негативные случаи), семантика /etc/hysteria/config.yaml против production-профиля, версия установленного бинаря. Состояние сервера ни одна из них не меняет.

Перезапуск сервисов остаётся операцией install, reconfigure --apply и repair — там он является частью применения изменений, а не проверкой.

13. Admin UI access via SSH tunnel

Production policy: UI остаётся loopback-only (HY2XS_UI_BIND_HOST=127.0.0.1), внешний доступ к 8080/tcp не открывается. Операторский доступ выполняется через SSH local forwarding.

Windows tunnel command:

ssh -p 2323 \
  -i C:\Users\kirap\.ssh\id_ed25519_uk1 \
  -N \
  -L 127.0.0.1:8080:127.0.0.1:8080 \
  root@185.156.108.141

Open in browser: http://127.0.0.1:8080/#/login.

Если туннель падает с administratively prohibited, проверить effective sshd-конфиг:

sshd -T | grep -E '^(port|allowtcpforwarding|permitopen|gatewayports|passwordauthentication|permitrootlogin) '

Recommended sshd hardening fragment:

Port 2323
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password

AllowTcpForwarding local
PermitOpen 127.0.0.1:8080 localhost:8080
GatewayPorts no

X11Forwarding no
AllowAgentForwarding no
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2

14. Secret-safe config sharing

Для передачи конфигов в тикеты/чаты используйте встроенную redaction-команду:

hy2xs-orchestrator redact-config --config /etc/hy2xs/hy2xs.env --out /root/hy2xs.redacted.env
hy2xs-orchestrator redact-config --config /etc/hysteria/post-install.env --out /root/post-install.redacted.env
hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/hysteria-config.redacted.yaml --format yaml

Инварианты:

  • команда не выводит исходные секреты в stdout;
  • требуется выбрать ровно один режим: --in-place или --out <path>;
  • --format auto пытается определить формат по имени файла, при неоднозначности используйте --format env|yaml;
  • YAML редактируется структурно (документ разбирается и обходится как дерево), поэтому вложенные секреты вроде auth.http.url?access_token=… не переживают редакцию, а результат остаётся валидным YAML;
  • в env-файлах секрет вырезается и из URL-значения, даже если имя ключа несекретное — например, HY2_AUTH_URL в post-install.env.

Та же редакция применяется к diagnostics-бандлу (hy2xs-orchestrator diagnostics collect), который собирается автоматически при неудачной установке или реконфигурации. Бандл предназначен для передачи наружу, поэтому попадающие в него hy2xs.env, post-install.env и config.yaml редактируются перед упаковкой.

При отказе до начала применения изменений (fatal_pre_apply) бандл не собирается: его сбор сам создал бы каталоги в /var/log/hy2xs на сервере, который мы обещали не трогать.