330a63b050
Верхнеуровневый откат стал неотменяемым в прошлом проходе, и на этом фоне
проявилось, что его substrate этой надёжности не соответствует: откат
гарантированно запускался, но отдельные его шаги могли молча не выполнить
восстановление, отчитаться успехом и уничтожить резервную копию.
1. Данные для отката уничтожались ДО фиксации успеха (commit ordering).
cancelFirewallRollback снимала таймер автоотката И удаляла резервные копии
firewall, а вызывалась до долговечной записи phase=installed. Отказ этой
записи (ENOSPC/EIO/read-only ФС) приводил в обработчик ошибки, обязательный
откат честно запускался и сообщал "no HY2XS rollback markers found":
откатывать было нечем. Причём отказ записи маркера — ровно тот сценарий,
который прошлый проход специально сделал безопасным.
Разделено на disarmFirewallRollback (снять таймер, копии оставить) и
cleanupFirewallRollback (удалить копии). Порядок в install и reconfigure:
smoke_ok -> disarm -> durable installed -> cleanup best-effort.
2. Резервные копии снимались без доказательства.
И firewall, и reconfigure копировали как `cp ... || true`: отказ
игнорировался, операция шла менять систему без копии, на которую
рассчитывает откат. У firewall маркер prepared («данные для отката
существуют») выставлялся вообще ДО копирования. Копирование строгое, факт
создания проверяется, маркер ставится после.
3. Копии reconfigure смешивались между операциями.
Общий набор *.bak в /etc/hy2xs/backups не был привязан к проходу. Если у
операции B копирование падало, B всё равно менял систему, а его откат
восстанавливал файлы операции A — сервер возвращался в более старое
состояние и это выглядело успешным откатом. Копия стала операционной:
/etc/hy2xs/backups/<op-id>/ с манифестом, где отсутствие файла записано
явно ("present": false), а не выведено из неудачи cp. Разбор строгий,
включая проверку opId.
4. Ошибка восстановления скрывалась, и после неё копии удалялись.
rollbackFirewallNow выполняла cp и nft -f с `|| true`, затем безусловно
удаляла /run/hy2xs/rollback/<op>. Худшая комбинация: неудача не видна,
стадия успешна, данные для ручной починки уничтожены. Теперь копии
удаляются только после подтверждённого успеха, иначе сохраняются с
сообщением manual recovery data preserved at ...
5. Команды отката глушили собственный код возврата.
До стадийного раннера `|| true` был единственной защитой от обрыва цепочки;
после его появления стал маскировкой — стадия не могла сообщить, что
ничего не сделала. Убран; rollbackCurrentState разбита на семь независимых
стадий.
6. Долговечность записи каталога маркера.
writeTextAtomic синхронизирует файл и его каталог, но при первой установке
/var/lib/hy2xs создаётся тут же, и запись "hy2xs" в /var/lib оставалась
несинхронизированной. ensureDir сообщает о фактическом создании и
синхронизирует родителя только тогда.
Отдельно про doctor. Утверждение аудита, что doctor вызывает
UpdatePeerLastConnectionAt через успешную machine-auth, кодом не
подтверждается: проба с действующим паролем ограничена `context.mode ===
"install"`, а doctor работает в режиме reconfigure. Инвариант, однако, ничем не
охранялся — добавлены тест и приёмка. Документация уточнена: guard действует
внутри процесса, а границу «что doctor шлёт по сети» держит состав проб;
единственный остающийся след — записи в журнале админки, и это сказано прямо.
Тесты: backup-integrity.test.ts (манифест, строгий разбор, копия до мутации,
сохранение копий при неудачном восстановлении), commit-ordering.test.ts
(disarm/cleanup разделены, порядок фиксации в обеих командах). Три теста,
закреплявших прежний инвариант «каждая команда отката несёт || true»,
переписаны на обратный: команды обязаны сообщать о своих отказах.
239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
# HY2XS production runbook
|
||
|
||
## 1. Supported target
|
||
|
||
- clean Debian 13 amd64
|
||
- single host install profile
|
||
- IPv4-only runtime model
|
||
|
||
## 2. Required prerequisites
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
./install.sh --non-interactive
|
||
```
|
||
|
||
Важно: packaged baseline использует `HY2XS_SSH_PORT=2323` по умолчанию.
|
||
На target-хосте оператор обязан выставить свой рабочий SSH-порт в `/etc/hy2xs/hy2xs.env`
|
||
и применить изменения через `reconfigure --apply`.
|
||
|
||
## 6. Post-install verification
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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` лежат по этому пути |
|
||
|
||
Резервные копии не удаляются, пока восстановление не подтверждено, и переживают
|
||
долговечную запись `phase: installed`. Поэтому при разборе неудачи всегда
|
||
осмысленно посмотреть:
|
||
|
||
```bash
|
||
ls -la /run/hy2xs/rollback/ # копии firewall текущей операции
|
||
ls -la /etc/hy2xs/backups/ # копия состояния до последнего reconfigure
|
||
cat /etc/hy2xs/backups/*/manifest.json
|
||
```
|
||
|
||
Манифест прямо говорит, какие файлы существовали до операции, а какие нет:
|
||
запись `"present": false` означает, что откат обязан был файл **удалить**, а не
|
||
восстановить.
|
||
|
||
Наружу оркестратор всегда пробрасывает **исходную** ошибку операции, а не
|
||
проблему внутри отката: последняя — это информация о том, что осталось не
|
||
восстановленным, а не причина отказа.
|
||
|
||
## 9. Reconfigure flow
|
||
|
||
```bash
|
||
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_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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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-конфиг:
|
||
|
||
```bash
|
||
sshd -T | grep -E '^(port|allowtcpforwarding|permitopen|gatewayports|passwordauthentication|permitrootlogin) '
|
||
```
|
||
|
||
Recommended sshd hardening fragment:
|
||
|
||
```sshconfig
|
||
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-команду:
|
||
|
||
```bash
|
||
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` на сервере,
|
||
который мы обещали не трогать.
|
||
|