Разбор предыдущего прохода со сверкой по исходникам 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>
14 KiB
Runtime env и post-install snapshot
Цель документа
Зафиксировать двухслойную модель:
- editable runtime env:
/etc/hy2xs/hy2xs.env - generated deploy snapshot:
/etc/hysteria/post-install.env
Зачем нужен файл
После первичной установки оператору нужны:
- runtime-файл, который оркестратор читает и валидирует;
- 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_OSDEPLOY_TIMESTAMP(last apply timestamp)PACKAGE_NAMEPACKAGE_BUILD_IDPACKAGE_VERSION
Orchestrator
ORCH_SOURCE_STACK=bun-typescriptORCH_BUILD_MODEORCH_BUILD_IDORCH_ENTRYPOINT
Общие
HY2XS_CONFIG_SCHEMA_VERSIONDEPLOY_DOMAINPUBLIC_HOSTPUBLIC_PORTSSH_PORTHY2XS_FIREWALL_MODEHY2XS_FIREWALL_STAGED_APPLYHY2XS_ADMIN_USERHY2XS_FORCE_PASSWORD_CHANGEHY2XS_ALLOW_SELF_SIGNED_DEV
Hysteria
HY2_SOURCE=official-upstreamHY2_VERSION— фактически установленная версияHY2_RESOLUTION— как версия была выбрана при сборке:latest-stable,pinnedилиoverrideHY2_TLS_MODEHY2_ACME_EMAILHY2_TLS_CERT_PATHHY2_TLS_KEY_PATHHY2_LISTEN_HOSTHY2_PORTHY2_AUTH_MODEHY2_AUTH_URLHY2_TRAFFIC_STATS_LISTENHY2_OBFS_TYPE—geckoилиsalamanderHY2_OBFS_PASSWORDHY2_GECKO_MIN_PACKET_SIZEHY2_GECKO_MAX_PACKET_SIZEHY2_BANDWIDTH_UPHY2_BANDWIDTH_DOWNHY2_DISABLE_LOSS_COMPENSATIONHY2_IGNORE_CLIENT_BANDWIDTHHY2_CONGESTION_TYPEHY2_BBR_PROFILEHY2_DISABLE_STATELESS_RESETHY2_CONFIG_PATH
HY2XS admin
HY2XS_ADMIN_ENABLEDHY2XS_ADMIN_SOURCEHY2XS_ADMIN_BUILD_IDHY2XS_ADMIN_BIND_HOSTHY2XS_ADMIN_PORTHY2XS_ADMIN_INSTALL_DIRHY2XS_ADMIN_DATA_DIRHY2XS_ADMIN_LOG_DIR
Как работать с файлами
Правильная модель:
- оркестратор создаёт
hy2xs.envиpost-install.envпри установке; - оператор редактирует только
hy2xs.env; - оператор запускает
reconfigure --dry-run, затемreconfigure --apply; - оркестратор обновляет 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 --applybootstrap 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.