Отзыв секрета не сходился: `auth_id` при смене секрета оставался прежним, поэтому сессия, установленная по отозванным учётным данным, была неотличима от законной, и цикл учёта не имел признака, по которому её следовало завершить. У состояния есть путь без единой неудачи — Hysteria регистрирует соединение в Traffic Stats API только после возврата backend-auth, поэтому успешный /kick может пройти мимо. Новое поколение credentials получает новый auth_id, kick идёт по старому, пережившая сессия становится orphan. Адрес Traffic Stats API имел два контракта: оркестратор принимал любой IPv4, админка всегда шла на loopback. Валидная по всем гейтам конфигурация выключала лимит устройств, учёт трафика и принудительное отключение разом. Адрес зафиксирован, а расхождение файла с ним админка называет. Состояние службы стало трёхзначным: util.Exec выбрасывал вывод systemctl при ненулевом коде, поэтому «остановлена» и «спросить не удалось» приходили одним значением, а доступность Traffic Stats API выводилась из него же. Журнал Hysteria разбирается в фактическом формате upstream (time — дробное число), страница конфигурации показывает файл вместо дефолтов UI и не возит секреты в браузер, санитайзер выгрузки следует по YAML-якорям. Разбор: docs/acceptance/2026-09-02-v1.0.0-rc4-preflight-findings.md
20 KiB
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 (timer accuracy 1s) |
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 |
unable to verify firewall rollback guard state; systemd query failed |
состояние 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/waiting)
Условие старта — не «PID предыдущей мёртв», а «у предыдущей не осталось
исполнителей, способных изменить систему». Ждать нужно не больше 45–46 секунд с
момента применения firewall; failed у guard покою не мешает и означает, что
пора смотреть journalctl -u 'hy2xs-fw-rollback-*' и запускать repair.
Второй отказ того же барьера выглядит иначе и требует другого действия:
unable to verify firewall rollback guard state; systemd query failed,
refusing to start a lifecycle operation
Здесь ждать бессмысленно. Барьер обязан доказать покой, а не предположить
его: отсутствие ответа systemd — это отсутствие наблюдения, а не наблюдение
отсутствия guard'а. Смотрите systemctl status и повторяйте операцию после того,
как systemd отвечает.
/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.
10a. Отзыв учётных данных пира
Смена секрета в панели — операция отзыва, и она выполняется целиком:
1. новый secret_digest и новый auth_id записываются одной операцией
2. POST /kick по СТАРОМУ auth_id
3. если разрыв не удался либо соединение зарегистрировалось уже после него —
старая сессия становится orphan и завершается очередным циклом учёта
Что это значит для оператора:
- гарантия отзыва — не позднее 30 секунд (интервал цикла учёта), а не «до переподключения клиента по своей воле»;
- частичный результат (
peer_disconnect_failed) означает лишь то, что первая попытка разрыва не удалась: повторять операцию не требуется, состояние сойдётся само; - идентификатор пира в списке (
authId) после смены секрета меняется — это идентичность поколения сессий, а не постоянный идентификатор записи; - трафик старой сессии за эти секунды не приписывается пиру и попадает в потери
цикла учёта (запись уровня
errorв журнале админки). Для операционной границы доступа это допустимо; биллингом учёт трафика в1.0.0не является.
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_HOST(иHY2XS_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 на сервере,
который мы обещали не трогать.