ab788725cf
Разбор предыдущего прохода со сверкой по исходникам systemd v257.13 — той самой линии, что стоит на Debian 13. Тема та же и слоем глубже: контракт, объявленный шире, чем его принимает чужая сторона. Прошлый проход сделал транспорт lossless для значений, которые systemd принимает, но не спросил, какие значения он принимает вообще. 1. Домен значений файла окружения Перед тем как принять пару, systemd прогоняет ключ и значение через utf8_is_valid (src/basic/env-file.c, check_utf8ness_and_warn), и отказ там возвращает -EINVAL — то есть НЕзагруженный EnvironmentFile= и юнит, который не стартует, а не предупреждение. unichar_is_valid (src/basic/utf8.c) отвергает суррогаты, U+FDD0..U+FDEF и все code points вида *FFFE/*FFFF, а сам utf8_is_valid — встроенный NUL и невалидный UTF-8. Пароль "abcde" + U+FDD0 — шесть символов, восемь байт, ни одного управляющего — проходил панель, оркестратор, DTO и хеширование, записывался в hy2xs.env, и после этого админка не поднималась. Тот же класс дефекта, ради уничтожения которого контракт и существует, только слоем ниже. Введён IsEnvTransportableText (Go) / isEnvTransportable (TS), повторяющий множество systemd точно — не шире и не уже. Отдельно отвергаются одиночные суррогаты: строка JavaScript вправе их содержать, а TextEncoder молча заменяет непарный суррогат на U+FFFD, то есть без проверки в файл уехал бы ДРУГОЙ секрет, а не отказ. Заодно разделены домен транспорта и политика продукта. Проверка отвергала C0 и DEL с формулировкой «формат управляющих символов не несёт» — неправда: внутри двойных кавычек перевод строки накапливается как обычный байт и переживает round-trip. Именно эта подмена и позволила проверке не знать про noncharacters. Политика HY2XS теперь запрещает категорию Cc целиком (была шире кода ровно на C1) плюс U+FEFF — последний отдельным решением продукта, а не форматом: 0xFEFF & 0xFFFE это 0xFEFE, и systemd такое значение принимает. 2. Рецепт восстановления выполнял env-файл как код В docs/operations/12, раздел «Забыт пароль администратора», стояло `set -a; . /etc/hy2xs/hy2xs.env; set +a`. Строка стала опасной ровно тогда, когда файл научился нести произвольные значения. Для systemd HY2XS_ADMIN_INITIAL_PASSWORD="$(...)" — буквальное значение: подстановок в EnvironmentFile= нет вовсе. Но `.` обрабатывает файл bash, а bash внутри двойных кавычек выполняет подстановку команд — от root, прямо в рецепте восстановления доступа. Соседний раздел той же страницы при этом уже правильно запрещал source/eval для bootstrap-admin.secret: документ запрещал действие и тут же его предлагал. Рецепт читает нужные значения как ДАННЫЕ. Поставлен гейт приёмки, запрещающий возврат source/./eval над этими файлами в командах документации и в скриптах; гейт смотрит только внутрь ```-блоков, чтобы объяснение, называющее убранную конструкцию по имени, его не роняло. 3. Отказ приходил после мутаций хоста Проверка транспорта жила только внутри renderRuntimeEnv, то есть срабатывала на шаге «write runtime env» — уже после bootstrap оркестратора, установки пакетов и раскладки файловой системы, — а read-only preflight-install говорил PASS: он зовёт parseRuntimeEnv и ничего не рендерит. Детерминированно известная ошибка конфигурации роняла операцию, оставив за собой изменённый хост, что прямо противоречит контракту PHASE 0. validateRuntimeEnvTransport вызывается теперь из parseRuntimeEnv и проходит по ВСЕМ парам runtimeEnvEntries: ограничение принадлежит формату, а не полю пароля, и HY2XS_ADMIN_CON_PASS сломал бы загрузку юнита так же. 4. Точность порта автомата и его описания - в состоянии DOUBLE_QUOTE_VALUE_ESCAPE systemd пишет `c != '\n'`, а не проверку на любой перевод строки (в VALUE_ESCAPE — наоборот, strchr(NEWLINE, c)). Порт съедал и \<LF>, и \<CR>; - комментарий обещал одно намеренное расхождение с systemd, а их два: кроме строки без `=`, HY2XS отказывает и на незакрытой кавычке в конце файла. Оба fail-closed и теперь названы оба. Тесты: граничная таблица во всех слоях дополнена значениями вне домена (U+FDD0, U+FDEF, U+FFFE, U+FFFF, U+1FFFF, U+10FFFF, невалидный UTF-8), соседями диапазонов (U+FDCF, U+FDF0, U+FFFD, U+10FFFD), C1 и U+FEFF, одиночным суррогатом. Добавлены TestEnvTransportDomainMatchesSystemd (домен не шире и не уже) и TestProductPolicyIsWiderThanTransportDomain (домен и политика различимы), а также проверки fail-closed порядка: parseRuntimeEnv отвергает непригодную конфигурацию, проверяются все значения файла, запись и проверка ходят по одному списку пар. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
249 lines
14 KiB
Markdown
249 lines
14 KiB
Markdown
# Runtime env и post-install snapshot
|
||
|
||
## Цель документа
|
||
|
||
Зафиксировать двухслойную модель:
|
||
|
||
- editable runtime env: `/etc/hy2xs/hy2xs.env`
|
||
- generated deploy snapshot: `/etc/hysteria/post-install.env`
|
||
|
||
## Зачем нужен файл
|
||
|
||
После первичной установки оператору нужны:
|
||
|
||
1) runtime-файл, который оркестратор читает и валидирует;
|
||
2) snapshot-файл фактического deploy-состояния.
|
||
|
||
В snapshot видно:
|
||
- какой пакет был установлен
|
||
- какой build артефакт использован
|
||
- какой стек оркестратора применён
|
||
- какая версия Hysteria реально установилась
|
||
- какой build HY2XS admin разложен на target
|
||
- какие базовые параметры сети и портов заданы
|
||
|
||
Именно для этого создаётся `post-install.env`.
|
||
|
||
## Чего файл не делает
|
||
|
||
Этот файл:
|
||
- не делает оркестратор update-manager'ом
|
||
- не гарантирует автоматическое применение изменений
|
||
- не заменяет runtime-конфиги
|
||
- не превращает target в builder
|
||
|
||
## Рекомендуемые пути
|
||
|
||
```bash
|
||
/etc/hy2xs/hy2xs.env
|
||
/etc/hysteria/post-install.env
|
||
```
|
||
|
||
`/etc/hy2xs/hy2xs.env` и `/etc/hysteria/post-install.env` должны иметь права `0600 root:root`.
|
||
|
||
`/etc/hysteria/config.yaml` должен иметь права `0640 hysteria:hy2xs-admin` (UI только читает).
|
||
|
||
## Минимальный набор переменных
|
||
|
||
### Deploy / package
|
||
- `DEPLOY_TARGET_OS`
|
||
- `DEPLOY_TIMESTAMP` (last apply timestamp)
|
||
- `PACKAGE_NAME`
|
||
- `PACKAGE_BUILD_ID`
|
||
- `PACKAGE_VERSION`
|
||
|
||
### Orchestrator
|
||
- `ORCH_SOURCE_STACK=bun-typescript`
|
||
- `ORCH_BUILD_MODE`
|
||
- `ORCH_BUILD_ID`
|
||
- `ORCH_ENTRYPOINT`
|
||
|
||
### Общие
|
||
- `HY2XS_CONFIG_SCHEMA_VERSION`
|
||
- `DEPLOY_DOMAIN`
|
||
- `PUBLIC_HOST`
|
||
- `PUBLIC_PORT`
|
||
- `SSH_PORT`
|
||
- `HY2XS_FIREWALL_MODE`
|
||
- `HY2XS_FIREWALL_STAGED_APPLY`
|
||
- `HY2XS_ADMIN_USER`
|
||
- `HY2XS_FORCE_PASSWORD_CHANGE`
|
||
- `HY2XS_ALLOW_SELF_SIGNED_DEV`
|
||
|
||
### Hysteria
|
||
- `HY2_SOURCE=official-upstream`
|
||
- `HY2_VERSION` — фактически установленная версия
|
||
- `HY2_RESOLUTION` — как версия была выбрана при сборке: `latest-stable`, `pinned` или `override`
|
||
- `HY2_TLS_MODE`
|
||
- `HY2_ACME_EMAIL`
|
||
- `HY2_TLS_CERT_PATH`
|
||
- `HY2_TLS_KEY_PATH`
|
||
- `HY2_LISTEN_HOST`
|
||
- `HY2_PORT`
|
||
- `HY2_AUTH_MODE`
|
||
- `HY2_AUTH_URL`
|
||
- `HY2_TRAFFIC_STATS_LISTEN`
|
||
- `HY2_OBFS_TYPE` — `gecko` или `salamander`
|
||
- `HY2_OBFS_PASSWORD`
|
||
- `HY2_GECKO_MIN_PACKET_SIZE`
|
||
- `HY2_GECKO_MAX_PACKET_SIZE`
|
||
- `HY2_BANDWIDTH_UP`
|
||
- `HY2_BANDWIDTH_DOWN`
|
||
- `HY2_DISABLE_LOSS_COMPENSATION`
|
||
- `HY2_IGNORE_CLIENT_BANDWIDTH`
|
||
- `HY2_CONGESTION_TYPE`
|
||
- `HY2_BBR_PROFILE`
|
||
- `HY2_DISABLE_STATELESS_RESET`
|
||
- `HY2_CONFIG_PATH`
|
||
|
||
### HY2XS admin
|
||
- `HY2XS_ADMIN_ENABLED`
|
||
- `HY2XS_ADMIN_SOURCE`
|
||
- `HY2XS_ADMIN_BUILD_ID`
|
||
- `HY2XS_ADMIN_BIND_HOST`
|
||
- `HY2XS_ADMIN_PORT`
|
||
- `HY2XS_ADMIN_INSTALL_DIR`
|
||
- `HY2XS_ADMIN_DATA_DIR`
|
||
- `HY2XS_ADMIN_LOG_DIR`
|
||
|
||
## Как работать с файлами
|
||
|
||
Правильная модель:
|
||
1. оркестратор создаёт `hy2xs.env` и `post-install.env` при установке;
|
||
2. оператор редактирует только `hy2xs.env`;
|
||
3. оператор запускает `reconfigure --dry-run`, затем `reconfigure --apply`;
|
||
4. оркестратор обновляет runtime и перезаписывает snapshot.
|
||
|
||
### Политики проверок DNS
|
||
|
||
В `/etc/hy2xs/hy2xs.env` есть две независимые политики, обе по умолчанию
|
||
`strict`:
|
||
|
||
| Переменная | Что проверяет |
|
||
| --- | --- |
|
||
| `HY2XS_DNS_AAAA_POLICY` | наличие AAAA-записи при IPv4-only профиле |
|
||
| `HY2XS_PUBLIC_ENDPOINT_POLICY` | что A-записи публичного endpoint ведут на публичные IPv4 этого сервера |
|
||
|
||
`HY2XS_PUBLIC_ENDPOINT_POLICY` принимает `strict` / `warn` / `off`. Ослаблять
|
||
её имеет смысл только для топологий вне baseline: сервер за NAT, floating IP,
|
||
anycast. Отсутствие A-записи фатально при любом значении.
|
||
|
||
Обе политики применяются в `install`, `reconfigure` и `doctor`, потому что
|
||
живут в общем `preflight`.
|
||
|
||
### Сетевая идентичность админки
|
||
|
||
`HY2XS_UI_PORT`, `HY2XS_UI_BIND_HOST`, `HY2XS_DATA_DIR` и `HY2XS_LOG_DIR` —
|
||
единственный источник истины для этих величин. Админка читает их из окружения
|
||
юнита и не хранит собственных копий в SQLite.
|
||
|
||
Важно:
|
||
- `HY2XS_ADMIN_INITIAL_PASSWORD` используется только для первичного bootstrap seed;
|
||
- `HY2XS_ADMIN_CON_PASS` — отдельная runtime-сущность для Hysteria auth/smoke;
|
||
- bootstrap secret хранится в том же формате `KEY=VALUE` (`ADMIN_USER`, `ADMIN_INITIAL_PASSWORD`, `ADMIN_CON_PASS`), права `0600`; значения с пробелами по краям, кавычками или обратными слешами записываются в двойных кавычках — читать файл следует парсером формата, а не `cut -d= -f2-`;
|
||
- `HY2XS_FORCE_PASSWORD_CHANGE` в production baseline установлен в `false` (forced UX-flow пока не реализован);
|
||
- после первичного seed перезапуски `hy2xs-admin` не должны переопределять пароль admin и `con_pass`.
|
||
|
||
### Учётные данные администратора проверяются при разборе окружения
|
||
|
||
`HY2XS_ADMIN_USER` и `HY2XS_ADMIN_INITIAL_PASSWORD` — это значения, которые
|
||
потом принимает **форма входа в панель**. Оркестратор проверяет их против того
|
||
же контракта, что и админка (`apps/credential/admin.go`):
|
||
|
||
| Переменная | Требование | Значение по умолчанию |
|
||
| --- | --- | --- |
|
||
| `HY2XS_ADMIN_USER` | 6-32 символа из набора `a-z A-Z 0-9 !@#$%^&*()_+,-./:;<=` | `hy2xsadmin` |
|
||
| `HY2XS_ADMIN_INITIAL_PASSWORD` | 6-64 символа Unicode **и** не более 72 байт в UTF-8; значение, загружаемое systemd из `EnvironmentFile=`; набор не ограничен, кроме `Cc` и `U+FEFF` | генерируется |
|
||
|
||
Значение вне контракта **роняет установку** с явным текстом, называющим границы
|
||
и набор. Так и должно быть: отказ, пришедший установщику, чинится одной строкой
|
||
в `hy2xs.env`, а неработающий вход на готовом сервере — переустановкой.
|
||
|
||
Проверяется и сгенерированный пароль, а не только заданный оператором:
|
||
генератор — такой же источник значения.
|
||
|
||
Границ у пароля **две**, и они в разных единицах. Предел в 72 байта ставит
|
||
bcrypt: `GenerateFromPassword` отвечает `ErrPasswordTooLong` на пароль длиннее
|
||
72 байт, а у 64 символов длина от 64 до 256 байт в зависимости от алфавита.
|
||
Пока байтовой границы здесь не было, `HY2XS_ADMIN_INITIAL_PASSWORD` из 64
|
||
кириллических букв (128 байт) проходил установку целиком, а первая учётная
|
||
запись администратора не создавалась вовсе — админка падала при старте. Подробно
|
||
границы описаны в `docs/admin/15-ui-contracts.md`.
|
||
|
||
Окружающие пробелы у `HY2XS_ADMIN_USER` снимаются. Иначе они уезжали бы в имя
|
||
учётной записи в SQLite, и вход отказывал бы «неверным логином или паролем» —
|
||
отказом, который невозможно связать с причиной.
|
||
|
||
У `HY2XS_ADMIN_INITIAL_PASSWORD` пробелы по краям, наоборот, **являются частью
|
||
пароля** и не снимаются нигде — ни оркестратором, ни админкой, ни хешированием.
|
||
Чтобы такое значение пережило запись и чтение, оно записывается **в двойных
|
||
кавычках** с экранированием `\` и `"`:
|
||
|
||
```text
|
||
HY2XS_ADMIN_INITIAL_PASSWORD="пароль с пробелом на конце "
|
||
```
|
||
|
||
Кавычки здесь не украшение. Файл читает не только оркестратор, но и systemd —
|
||
он объявлен `EnvironmentFile=` в юните `hy2xs-admin`, — а у **незакавыченного**
|
||
значения systemd срезает пробелы по краям и трактует `\` как escape. Если вы
|
||
правите `hy2xs.env` руками и в пароле есть пробел по краям, кавычка или обратный
|
||
слеш, закавычьте значение тем же способом. Обычные значения (порты, пути,
|
||
домены) кавычек не требуют и записываются как раньше.
|
||
|
||
Есть два ограничения набора символов, и они разного происхождения.
|
||
|
||
**Домен systemd.** Значение обязано быть загружаемым из `EnvironmentFile=`:
|
||
валидный UTF-8 из Unicode scalar values, без NUL, без суррогатов и без
|
||
noncharacters (`U+FDD0..U+FDEF` и все `*FFFE`/`*FFFF`). Это не наше правило —
|
||
systemd прогоняет значение через `utf8_is_valid` и отвечает `-EINVAL`, то есть
|
||
файл окружения **не загружается** и юнит не стартует. Оркестратор проверяет
|
||
домен на **каждом** значении файла, а не только на пароле: `HY2XS_ADMIN_CON_PASS`
|
||
или obfs-пароль сломали бы загрузку юнита ровно так же.
|
||
|
||
**Политика HY2XS.** Сверх этого запрещены управляющие символы Unicode
|
||
(категория `Cc`) и `U+FEFF`. Формат их несёт, но ввести такой пароль в
|
||
однострочное поле формы входа всё равно нельзя.
|
||
|
||
Отказ по любому из двух правил приходит **при разборе конфигурации**, то есть до
|
||
первой необратимой операции над хостом: `preflight-install` и `install` видят
|
||
его одинаково.
|
||
|
||
Значение по умолчанию совпадает в трёх местах и обязано совпадать:
|
||
`package/config/hy2xs.env`, `orchestrator/src/config/env.ts` и запасное
|
||
значение в `apps/dao/sqlite.go`. Раньше оркестратор писал `admin` — пять
|
||
символов при минимуме панели в шесть, — и установка завершалась
|
||
`INSTALL EXIT CODE: 0`, оставляя панель, в которую невозможно войти.
|
||
|
||
### Backlog: секреты в окружении
|
||
|
||
Документация systemd отдельно рекомендует **не** передавать секреты через
|
||
переменные окружения и предлагает для них `LoadCredential=` /
|
||
`LoadCredentialEncrypted=`: окружение процесса видно шире, чем файл с правами
|
||
`0600`.
|
||
|
||
HY2XS v1 этим не пользуется, и это осознанное решение по срокам, а не
|
||
недосмотр: переход затрагивает модель секретов всего продукта (машинный токен
|
||
Hysteria, obfs-пароль, `con_pass`), а не только пароль администратора.
|
||
Действующая защита — права `0600`, владелец `root:root` и отсутствие доступа у
|
||
служебных пользователей (`hy2xs-admin` и `hysteria` файл прочитать не могут, что
|
||
проверяет smoke). Пункт остаётся в backlog как отдельная работа.
|
||
|
||
### Immutable-bootstrap контракт
|
||
|
||
- `/etc/hy2xs/bootstrap-admin.secret` создаётся оркестратором только при первичной установке.
|
||
- На `reconfigure --apply` bootstrap secret не пересоздаётся и не ротируется автоматически.
|
||
- Изменения `HY2XS_ADMIN_INITIAL_PASSWORD` в runtime env после первичной установки не должны менять фактический пароль admin.
|
||
- `HY2XS_ADMIN_CON_PASS` используется как bootstrap-значение при первичной установке; после создания admin account изменение этого значения в `/etc/hy2xs/hy2xs.env` не пересоздаёт и не обновляет существующий `con_pass` в SQLite.
|
||
- Ротация `con_pass` выполняется через account-management слой UI/БД, а bootstrap snapshot остаётся неизменным.
|
||
|
||
## Что нельзя делать
|
||
|
||
- сваливать туда временный мусор
|
||
- считать, что edit env автоматически меняет runtime без `reconfigure --apply`
|
||
- использовать файл как замену настоящей конфигурации сервисов
|
||
|
||
## Пример
|
||
|
||
Используйте canonical runtime-файл `package/config/hy2xs.env` как базовый шаблон значений
|
||
и переносите его параметры в `/etc/hy2xs/hy2xs.env`.
|