fix(installer): harden admin smoke and rollback cleanup
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Результаты runtime-проверки v1.0.0-rc3
|
||||
|
||||
Дата проверки: 2026-09-07.
|
||||
|
||||
RC3 проверялся на чистом Debian после успешной проверки целостности
|
||||
опубликованного артефакта. Hysteria, ACME, firewall, диагностика и основной
|
||||
authentication backend отработали штатно. Кандидат отклонён из-за несовместимости
|
||||
installer smoke с HTTP middleware и ложного отчёта rollback cleanup.
|
||||
|
||||
## Сводка
|
||||
|
||||
| ID | Наблюдение | Итог |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| RC3-LOGIN-01 | стандартный `curl/<version>` получает HTTP 403 от scanner middleware | исправлено для RC4 явным UA установщика |
|
||||
| RC3-LOGIN-02 | запрос с полем `password` не проходит DTO: требуется `pass` | ожидаемое поведение wire-контракта, добавлена регрессия |
|
||||
| RC3-LOGIN-03 | `{username, pass}` с неверным паролем доходит до auth logic и возвращает `invalid_credentials` | backend исправен |
|
||||
| RC3-LOGIN-04 | positive и negative smoke имели две независимые curl-команды | исправлено общим request helper'ом |
|
||||
| RC3-ROLLBACK-01 | `reset-failed` для уже выгруженного юнита создавал ложное `manual recovery may be required` | исправлено проверкой postcondition |
|
||||
|
||||
## Что доказала диагностика login
|
||||
|
||||
Последовательность запросов отделила middleware, DTO и authentication logic:
|
||||
|
||||
| Запрос | Результат |
|
||||
| ---------------------------------------- | -------------------------------------------------------- |
|
||||
| стандартный curl UA + `password` | HTTP 403 до разбора тела |
|
||||
| допустимый UA + `password` | validation envelope: требуется поле `pass` |
|
||||
| допустимый UA + `pass` и неверный пароль | operation envelope с `errors[].code=invalid_credentials` |
|
||||
|
||||
Следовательно, паника старого `validateStr` не вернулась. Дефект RC3 находился
|
||||
между smoke-клиентом и scanner middleware, а не в проверке пароля.
|
||||
|
||||
## Контракт RC4
|
||||
|
||||
- positive и negative login используют один helper;
|
||||
- helper отправляет JSON `{username, pass}` и `User-Agent: HY2XS-Installer/1.0`;
|
||||
- success требует `code=20000` и непустой `data.accessToken`;
|
||||
- rejection требует `code=50000`, причину `invalid_credentials` внутри
|
||||
`errors[]` и отсутствие выданного токена;
|
||||
- envelope разбирается как JSON, поэтому совпадение текста внутри `message` не
|
||||
считается доказательством;
|
||||
- стандартный curl UA продолжает получать 403: scanner middleware в RC4 не
|
||||
удаляется;
|
||||
- уже выгруженный systemd-юнит считается успешным конечным состоянием cleanup
|
||||
только после наблюдения `LoadState` и `ActiveState`;
|
||||
- недоступный systemd или сохранившийся `ActiveState=failed` остаются настоящим
|
||||
rollback failure.
|
||||
|
||||
Пересмотр необходимости scanner middleware для loopback-only панели остаётся
|
||||
отдельной задачей после v1.0.0 и не расширяет change surface RC4.
|
||||
@@ -26,6 +26,7 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2026-09-01 | `1.0.0-rc1` | `a1f0db22` | build + host acceptance, Debian 13 | [RC ACCEPTED WITH RELEASE-REQUIRED UX FIXES](2026-09-01-v1.0.0-rc1-host-acceptance.md) |
|
||||
| 2026-09-04 | `1.0.0-rc2` | `82e5ca40` | clean install + runtime, Debian 13 | [RUNTIME REJECTED — вход в панель недоступен](2026-09-04-v1.0.0-rc2-runtime-findings.md) |
|
||||
| 2026-09-07 | `1.0.0-rc3` | `bf10810c` | clean install + failure rollback, Debian 13 | [RUNTIME REJECTED — installer smoke блокируется middleware](2026-09-07-v1.0.0-rc3-runtime-findings.md) |
|
||||
|
||||
## Открытые дефекты приёмки
|
||||
|
||||
@@ -33,6 +34,7 @@
|
||||
| --- | --- |
|
||||
| 2026-09-01, `1.0.0-rc1` | [UX-01…UX-05 и найденное сверх отчёта](2026-09-01-v1.0.0-rc1-ux-findings.md) |
|
||||
| 2026-09-04, `1.0.0-rc2` | [LOGIN-01…LOGIN-08](2026-09-04-v1.0.0-rc2-runtime-findings.md) — все закрыты |
|
||||
| 2026-09-07, `1.0.0-rc3` | [RC3-LOGIN-01…04, RC3-ROLLBACK-01](2026-09-07-v1.0.0-rc3-runtime-findings.md) — исправления подготовлены для RC4 |
|
||||
|
||||
## Разборы кода между прогонами
|
||||
|
||||
|
||||
@@ -74,18 +74,19 @@ sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env
|
||||
быть отменён: ни неудачной записью состояния в `/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, а не ждать |
|
||||
| Строка в журнале | Что она означает |
|
||||
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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` | восстановление отработало полностью |
|
||||
| `systemctl reset-failed <unit> exited with …, but the cleanup postcondition is satisfied` | юнит уже выгружен или не находится в `failed`; это допустимый идемпотентный cleanup, ручное восстановление не требуется |
|
||||
| `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 и не обязано его покрывать: доказательством служит не время, а маркер
|
||||
@@ -336,6 +337,7 @@ hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/
|
||||
```
|
||||
|
||||
Инварианты:
|
||||
|
||||
- команда не выводит исходные секреты в stdout;
|
||||
- требуется выбрать ровно один режим: `--in-place` или `--out <path>`;
|
||||
- `--format auto` пытается определить формат по имени файла, при неоднозначности используйте `--format env|yaml`;
|
||||
@@ -357,4 +359,3 @@ hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/
|
||||
При отказе **до** начала применения изменений (`fatal_pre_apply`) бандл не
|
||||
собирается: его сбор сам создал бы каталог в `/var/lib/hy2xs/diagnostics` на сервере,
|
||||
который мы обещали не трогать.
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## B. Target install tests
|
||||
|
||||
### На чистом Debian 13 проверяем
|
||||
|
||||
1. пакет запускается без ручной сборки на сервере
|
||||
2. Hysteria2 скачивается с official upstream
|
||||
3. bundled HY2XS admin раскладывается локально из пакета
|
||||
@@ -55,8 +56,8 @@
|
||||
35. **оператор входит в панель**: `POST /api/auth/login` с bootstrap-учётными данными из `/etc/hy2xs/bootstrap-admin.secret` отвечает `code: 20000` и непустым `accessToken`. Заведомо неверные учётные данные (со СЛУЧАЙНЫМ паролем, а не с литералом) дают HTTP 200 с конвертом отказа: `code: 50000`, причина `invalid_credentials` и отсутствие `accessToken`
|
||||
36. пароль предельной длины, назначенный формой смены пароля, принимается формой входа: границы обеих форм совпадают с серверными. Границ **две** — 6-64 символа Unicode и не более 72 байт в UTF-8 (предел bcrypt): пароль из 36 кириллических букв (72 байта) принимается, из 37 (74 байта) — отвергается конвертом валидации, а не системной ошибкой
|
||||
37. `HY2XS_ADMIN_INITIAL_PASSWORD` с пробелом по краям доезжает до учётной записи неизменным: значение записано в `hy2xs.env` в двойных кавычках, и вход выполняется ровно им, а не обрезанным
|
||||
37a. `HY2XS_ADMIN_INITIAL_PASSWORD` со значением вне документированного домена systemd (`U+FEFF`, `U+FDD0`, `U+FFFF`) или файл с байтами `0xFF`, оборванной `0xC3`, `ED A0 80` **роняет `preflight-install`** — отказ приходит до первой мутации хоста; сервер остаётся нетронутым
|
||||
37b. API входа и импорта отвергает повреждённый UTF-8 и непарные UTF-16 surrogate escape до `encoding/json`; настоящий U+FFFD работает как обычный символ
|
||||
37a. `HY2XS_ADMIN_INITIAL_PASSWORD` со значением вне документированного домена systemd (`U+FEFF`, `U+FDD0`, `U+FFFF`) или файл с байтами `0xFF`, оборванной `0xC3`, `ED A0 80` **роняет `preflight-install`** — отказ приходит до первой мутации хоста; сервер остаётся нетронутым
|
||||
37b. API входа и импорта отвергает повреждённый UTF-8 и непарные UTF-16 surrogate escape до `encoding/json`; настоящий U+FFFD работает как обычный символ
|
||||
38. `last_login_at` администратора обновляется после успешного входа и не меняется после неудачной попытки
|
||||
|
||||
## C0. Панель обязана впускать, а не слушать порт
|
||||
@@ -93,6 +94,7 @@ BOOTSTRAP_PASS="$(read_bootstrap_field ADMIN_INITIAL_PASSWORD)"
|
||||
|
||||
# Положительная проба: конверт успеха и выданный токен.
|
||||
curl -sS --max-time 5 -X POST \
|
||||
--user-agent 'HY2XS-Installer/1.0' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -nc --arg u "$BOOTSTRAP_USER" --arg p "$BOOTSTRAP_PASS" '{username:$u,pass:$p}')" \
|
||||
http://127.0.0.1:8080/api/auth/login | jq '.code, (.data.accessToken | length)'
|
||||
@@ -100,6 +102,7 @@ curl -sS --max-time 5 -X POST \
|
||||
# Отрицательная проба: пароль СЛУЧАЙНЫЙ, а проверяется конверт отказа целиком.
|
||||
# HTTP 200 сам по себе ничего не доказывает — админка отвечает 200 и на успех.
|
||||
curl -sS --max-time 5 -X POST \
|
||||
--user-agent 'HY2XS-Installer/1.0' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -nc --arg u "$BOOTSTRAP_USER" --arg p "$(head -c 18 /dev/urandom | base64)" '{username:$u,pass:$p}')" \
|
||||
http://127.0.0.1:8080/api/auth/login \
|
||||
@@ -110,7 +113,9 @@ unset BOOTSTRAP_PASS
|
||||
```
|
||||
|
||||
Ожидается `20000` и ненулевая длина токена. Сам токен не печатается: это
|
||||
действующая сессия администратора.
|
||||
действующая сессия администратора. Явный User-Agent обязателен: стандартный
|
||||
`curl/<version>` осознанно отклоняется scanner middleware с HTTP 403 ещё до
|
||||
разбора JSON. Значение совпадает с production smoke и не маскируется под браузер.
|
||||
|
||||
## C1. Семантический smoke конфига
|
||||
|
||||
|
||||
Reference in New Issue
Block a user