Files
HY2XS_flamy/docs/runtime/09-post-install-env.md
T
Crimson ab788725cf fix(env): контракт был шире домена, который принимает systemd
Разбор предыдущего прохода со сверкой по исходникам 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>
2026-09-06 23:38:57 +05:00

14 KiB
Raw Blame History

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

Рекомендуемые пути

/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_TYPEgecko или 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 пробелы по краям, наоборот, являются частью пароля и не снимаются нигде — ни оркестратором, ни админкой, ни хешированием. Чтобы такое значение пережило запись и чтение, оно записывается в двойных кавычках с экранированием \ и ":

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.