fix(installer): harden admin smoke and rollback cleanup
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
## Технологический стек оркестратора
|
||||
|
||||
Оркестратор фиксируется как:
|
||||
|
||||
- **Bun + TypeScript** по исходникам
|
||||
- локальная сборка builder layer'ом
|
||||
- поставка на target в виде **готового install-артефакта**
|
||||
|
||||
Это означает:
|
||||
|
||||
- на target нет `npm`, `pnpm`, `yarn` или `bun install`
|
||||
- на target нет transpile/build step
|
||||
- shell на target допустим только как thin wrapper entrypoint
|
||||
@@ -19,6 +21,7 @@
|
||||
## Главная роль оркестратора
|
||||
|
||||
Оркестратор работает **только на target machine** и умеет:
|
||||
|
||||
- выполнить read-only проверку чистоты хоста (`preflight-install`)
|
||||
- выполнить первичную установку (`install`)
|
||||
- выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`)
|
||||
@@ -42,6 +45,7 @@
|
||||
## Предусловия
|
||||
|
||||
Оркестратор рассчитан только на:
|
||||
|
||||
- чистый Debian 13
|
||||
- root/sudo install context
|
||||
- один сервер
|
||||
@@ -127,10 +131,10 @@ clean-host. Ко второму вызову на диске лежал собс
|
||||
Guard умеет останавливать только то, что через него проходит. Поэтому
|
||||
универсального раннера в `lib/process.ts` нет — есть два явных набора:
|
||||
|
||||
| Набор | Guard | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `runReadOnly`, `runReadOnlySecret` | не трогает | наблюдение за системой: `ss`, `systemctl is-active`, `curl`, `getent` |
|
||||
| `runMutating`, `runMutatingVisible`, `runMutatingHidden`, `runMutatingRaw` | спрашивает разрешение | всё, что может изменить хост |
|
||||
| Набор | Guard | Назначение |
|
||||
| -------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------- |
|
||||
| `runReadOnly`, `runReadOnlySecret` | не трогает | наблюдение за системой: `ss`, `systemctl is-active`, `curl`, `getent` |
|
||||
| `runMutating`, `runMutatingVisible`, `runMutatingHidden`, `runMutatingRaw` | спрашивает разрешение | всё, что может изменить хост |
|
||||
|
||||
`*Secret`-варианты не печатают команду в текст ошибки: их аргументы несут
|
||||
machine token или пароль пира, а сообщение уходит в логи и диагностику.
|
||||
@@ -277,10 +281,10 @@ preflight-ошибка со словом `nftables` приводила к отк
|
||||
|
||||
Второй инвариант — **стадии отката независимы**:
|
||||
|
||||
| Команда | Стадии |
|
||||
| --- | --- |
|
||||
| `install` | firewall → stop services → disable services → reset failed services |
|
||||
| `reconfigure` | firewall → restore configuration |
|
||||
| Команда | Стадии |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `install` | firewall → stop services → disable services → reset failed `hysteria-server` → reset failed `hy2xs-admin` |
|
||||
| `reconfigure` | firewall → restore configuration |
|
||||
|
||||
Каждая стадия — это `systemctl`, `cp`, `rm -rf` или `nft`, то есть каждая умеет
|
||||
упасть сама. Пока они стояли цепочкой `await`, отказ первой отменял все
|
||||
@@ -294,6 +298,14 @@ preflight-ошибка со словом `nftables` приводила к отк
|
||||
ошибка операции: проблема внутри отката — это дополнительная информация о том,
|
||||
что осталось не восстановленным, а не замена диагноза.
|
||||
|
||||
`reset-failed` для каждого сервиса является отдельной стадией и завершается
|
||||
проверкой `LoadState`/`ActiveState`. Ненулевой код команды допустим, если юнит
|
||||
уже выгружен (`not-found` + `inactive`): failed-состояния у него больше нет, а
|
||||
значит cleanup завершён. Текст `Unit … not loaded` намеренно не разбирается — он
|
||||
зависит от версии и локали systemd. Ошибка чтения состояния или сохранившийся
|
||||
`ActiveState=failed` остаются настоящим отказом и попадают в manual-recovery
|
||||
сводку.
|
||||
|
||||
Команды внутри стадий **не глушат собственные ошибки**. Это правило обратно
|
||||
тому, что действовало раньше. Пока непрерывность держалась на `|| true` в каждой
|
||||
команде, стадия физически не могла сообщить, что восстановление не выполнилось:
|
||||
@@ -368,7 +380,11 @@ reconfigure B → создание копии упало, ошибка скры
|
||||
"version": 1,
|
||||
"opId": "2026-08-30T10-00-00.000Z",
|
||||
"entries": [
|
||||
{ "path": "/etc/hysteria/config.yaml", "present": true, "stored": "etc_hysteria_config.yaml" },
|
||||
{
|
||||
"path": "/etc/hysteria/config.yaml",
|
||||
"present": true,
|
||||
"stored": "etc_hysteria_config.yaml"
|
||||
},
|
||||
{ "path": "/etc/nftables.d/hy2xs.nft", "present": false, "stored": null }
|
||||
]
|
||||
}
|
||||
@@ -423,11 +439,11 @@ preflight общий для `install`, `reconfigure` и `doctor`, инвариа
|
||||
|
||||
Строгость управляется `HY2XS_PUBLIC_ENDPOINT_POLICY`:
|
||||
|
||||
| Значение | Поведение |
|
||||
| --- | --- |
|
||||
| `strict` (по умолчанию) | расхождение останавливает операцию |
|
||||
| `warn` | печатается предупреждение, операция продолжается |
|
||||
| `off` | сравнение не выполняется |
|
||||
| Значение | Поведение |
|
||||
| ----------------------- | ------------------------------------------------ |
|
||||
| `strict` (по умолчанию) | расхождение останавливает операцию |
|
||||
| `warn` | печатается предупреждение, операция продолжается |
|
||||
| `off` | сравнение не выполняется |
|
||||
|
||||
Ослабление предназначено для топологий вне baseline (NAT, floating IP, anycast).
|
||||
Отсутствие A-записи остаётся фатальным при любом значении: имя без A-записи не
|
||||
@@ -436,6 +452,7 @@ preflight общий для `install`, `reconfigure` и `doctor`, инвариа
|
||||
## Что приходит на target
|
||||
|
||||
На target должен попадать уже готовый package, содержащий:
|
||||
|
||||
- thin install entrypoint
|
||||
- compiled orchestrator artifact
|
||||
- bundled HY2XS admin
|
||||
@@ -447,6 +464,7 @@ preflight общий для `install`, `reconfigure` и `doctor`, инвариа
|
||||
## Логическая модульность
|
||||
|
||||
Даже если на target приезжает один собранный артефакт, внутри исходников оркестратор должен быть разложен по шагам:
|
||||
|
||||
- preflight
|
||||
- deps
|
||||
- filesystem
|
||||
@@ -474,17 +492,20 @@ preflight общий для `install`, `reconfigure` и `doctor`, инвариа
|
||||
## Модель поставки
|
||||
|
||||
Рекомендуемая baseline-модель:
|
||||
|
||||
- исходники оркестратора хранятся в `orchestrator/`
|
||||
- builder выполняет локальную сборку через Bun
|
||||
- в install package кладётся готовый артефакт, который запускается thin wrapper'ом
|
||||
|
||||
Например:
|
||||
|
||||
- `package/install.sh` — проверка контекста и вызов оркестратора
|
||||
- `package/orchestrator/hy2xs-orchestrator` — собранный артефакт
|
||||
|
||||
## Логирование и коды возврата
|
||||
|
||||
Оркестратор должен:
|
||||
|
||||
- печатать понятные step-based сообщения
|
||||
- завершаться ненулевым кодом при ошибке
|
||||
- не скрывать первичный источник падения
|
||||
@@ -500,6 +521,7 @@ preflight общий для `install`, `reconfigure` и `doctor`, инвариа
|
||||
## CLI baseline
|
||||
|
||||
Команды:
|
||||
|
||||
- `preflight-install --package-dir <path> [--config <source-env>]`
|
||||
- `install --package-dir <path> [--config <source-env>]`
|
||||
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run`
|
||||
@@ -513,6 +535,7 @@ PHASE 0 ничего не меняет.
|
||||
`--allow-partial-state` допустим только для `repair`.
|
||||
|
||||
Инварианты:
|
||||
|
||||
- только IPv4 bind/listen;
|
||||
- TLS modes: `acme | file | self_signed_dev`;
|
||||
- `trafficStats.secret` отдельный от `JWT_SECRET`;
|
||||
@@ -645,10 +668,10 @@ EnvironmentFile дополнительно запрещает U+FEFF. Реали
|
||||
|
||||
Поэтому smoke выполняет **настоящий вход** на `POST /api/auth/login`:
|
||||
|
||||
| Проба | Когда | Что требуется |
|
||||
| --- | --- | --- |
|
||||
| настоящий логин + СЛУЧАЙНЫЙ пароль | всегда | `code: 50000`, причина `invalid_credentials`, `accessToken` отсутствует |
|
||||
| bootstrap-учётные данные из `bootstrap-admin.secret` | только `install` | `code: 20000` и непустой `accessToken` |
|
||||
| Проба | Когда | Что требуется |
|
||||
| ---------------------------------------------------- | ---------------- | ----------------------------------------------------------------------- |
|
||||
| настоящий логин + СЛУЧАЙНЫЙ пароль | всегда | `code: 50000`, причина `invalid_credentials`, `accessToken` отсутствует |
|
||||
| bootstrap-учётные данные из `bootstrap-admin.secret` | только `install` | `code: 20000` и непустой `accessToken` |
|
||||
|
||||
Детали, которые здесь существенны:
|
||||
|
||||
@@ -661,6 +684,16 @@ EnvironmentFile дополнительно запрещает U+FEFF. Реали
|
||||
`50000` (отказ операции, а не успех и не отказ валидации, который означал бы
|
||||
негодный запрос), доменная причина `invalid_credentials` и ОТСУТСТВИЕ
|
||||
`accessToken`;
|
||||
- **конверт разбирается как JSON**, а не ищется регулярным выражением в сыром
|
||||
тексте. Подстрока `invalid_credentials` внутри `message` или сломанный JSON не
|
||||
имеют права превратить неизвестный ответ в успешную проверку;
|
||||
- **обе пробы используют один request helper.** Wire-поле называется `pass`, а
|
||||
не `password`; `Content-Type`, User-Agent и настройки curl не дублируются и не
|
||||
могут разойтись между positive и negative ветками;
|
||||
- **smoke отправляет явный `HY2XS-Installer/1.0` User-Agent.** Стандартный
|
||||
`curl/<version>` отклоняется действующим scanner middleware раньше DTO. UA
|
||||
установщика называется своим именем, не имитирует браузер и при этом проходит
|
||||
существующий фильтр;
|
||||
- **пароль отрицательной пробы генерируется**, а не записан литералом. Записанное
|
||||
в исходнике значение теоретически может оказаться настоящим паролем — и тогда
|
||||
проверка «неверные данные отвергаются» отчиталась бы об успешном входе. На
|
||||
@@ -672,10 +705,10 @@ EnvironmentFile дополнительно запрещает U+FEFF. Реали
|
||||
объявила бы рабочую установку сломанной;
|
||||
- **токен требуется отдельно.** `code: 20000` без `accessToken` означал бы
|
||||
панель, которая пускает и не выдаёт сессию;
|
||||
- **тело собирается `JSON.stringify`**, а не интерполяцией в строку: пароль
|
||||
- **тело общего helper'а собирается `JSON.stringify`**, а не интерполяцией в строку: пароль
|
||||
задаёт оператор, и кавычка в нём сломала бы сам запрос, а не панель — проверка
|
||||
объявила бы рабочую установку сломанной;
|
||||
- **обе команды идут через `runReadOnlySecret`**: он не кладёт команду в текст
|
||||
- **общий helper идёт через `runReadOnlySecret`**: он не кладёт команду в текст
|
||||
ошибки, а команда несёт пароль администратора. Наружу отдаётся только код
|
||||
ответа: тело успешного входа содержит токен доступа, а текст ошибки уезжает в
|
||||
журнал установки и в diagnostics-бандл;
|
||||
@@ -712,6 +745,7 @@ service-writable `HY2XS_LOG_DIR`. Родитель проверяется чер
|
||||
дерево.
|
||||
|
||||
Редактируются:
|
||||
|
||||
- поля с секретоподобным именем (`password`, `secret`, `token`, `apiKey`,
|
||||
`privateKey`, `authorization`, `cookie`, `bearer`, `signature`, …);
|
||||
- карты, где секретны все значения (`auth.userpass`, `acme.dns.config`);
|
||||
|
||||
Reference in New Issue
Block a user