fix: harden runtime policy and remove legacy hysteria mutation paths

This commit is contained in:
2026-04-28 17:10:29 +05:00
parent 7734a76c39
commit 607689df9b
10 changed files with 38 additions and 90 deletions
+9 -1
View File
@@ -111,7 +111,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
domain: env.HY2XS_DOMAIN || "",
publicHost: normalizePublicHost(env.HY2XS_PUBLIC_HOST || env.HY2XS_DOMAIN || ""),
publicPort: parsePort("HY2XS_PUBLIC_PORT", env.HY2XS_PUBLIC_PORT, hysteriaPort),
ipv6Enabled: false,
ipv6Enabled: parseBool("HY2XS_IPV6_ENABLED", env.HY2XS_IPV6_ENABLED, false),
sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22),
firewallEnabled: parseBool("HY2XS_FIREWALL_ENABLED", env.HY2XS_FIREWALL_ENABLED, true),
firewallStagedApply: parseBool("HY2XS_FIREWALL_STAGED_APPLY", env.HY2XS_FIREWALL_STAGED_APPLY, true),
@@ -121,6 +121,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)),
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
allowSelfSignedDev: parseBool("HY2XS_ALLOW_SELF_SIGNED_DEV", env.HY2XS_ALLOW_SELF_SIGNED_DEV, false),
tlsMode,
acmeType,
acmeEmail: env.HY2XS_ACME_EMAIL || "",
@@ -178,11 +179,17 @@ export function validateRuntimeConfig(config: RuntimeConfig): void {
if (!config.acmeEmail) {
throw new Error("HY2XS_ACME_EMAIL is required for TLS mode acme");
}
if (config.acmeType === "dns") {
throw new Error("HY2XS_ACME_TYPE=dns is not supported in production profile yet");
}
}
if (config.tlsMode === "file") {
requireValue("HY2XS_TLS_CERT_PATH", config.tlsCertPath);
requireValue("HY2XS_TLS_KEY_PATH", config.tlsKeyPath);
}
if (config.tlsMode === "self_signed_dev" && !config.allowSelfSignedDev) {
throw new Error("HY2XS_TLS_MODE=self_signed_dev requires HY2XS_ALLOW_SELF_SIGNED_DEV=true");
}
if (config.publicHost === "0.0.0.0") {
throw new Error("HY2XS_PUBLIC_HOST must be a routable domain or IPv4, not 0.0.0.0");
@@ -205,6 +212,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
`HY2XS_ADMIN_CON_PASS=${config.adminConPass}`,
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
`HY2XS_ALLOW_SELF_SIGNED_DEV=${config.allowSelfSignedDev}`,
`HY2XS_TLS_MODE=${config.tlsMode}`,
`HY2XS_ACME_TYPE=${config.acmeType}`,
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
+1
View File
@@ -22,6 +22,7 @@ export async function generateConfig(context: InstallContext): Promise<void> {
UI_PORT: context.config.uiPort,
BANDWIDTH_UP: context.config.hysteriaBandwidthUp,
BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown,
IGNORE_CLIENT_BANDWIDTH: context.config.hysteriaIgnoreClientBandwidth ? "true" : "false",
TLS_ACME_BLOCK: tlsAcmeBlock,
TLS_FILE_BLOCK: tlsFileBlock,
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
+9 -1
View File
@@ -30,5 +30,13 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
});
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
await writeText(context.config.bootstrapAdminSecretPath, `${context.config.adminUser}:${context.config.adminConPass}\n`, 0o600);
await writeText(
context.config.bootstrapAdminSecretPath,
[
`ADMIN_USER=${context.config.adminUser}`,
`ADMIN_INITIAL_PASSWORD=${context.config.adminInitialPassword}`,
`ADMIN_CON_PASS=${context.config.adminConPass}`
].join("\n") + "\n",
0o600
);
}
+5 -3
View File
@@ -8,7 +8,7 @@ export async function smoke(context: InstallContext): Promise<void> {
return;
}
await runVisible`systemctl start hysteria-server hy2xs-admin`;
await runVisible`systemctl restart hysteria-server hy2xs-admin`;
await runVisible`systemctl is-active --quiet hysteria-server`;
await runVisible`systemctl is-active --quiet hy2xs-admin`;
await runVisible`/usr/local/bin/hysteria version`;
@@ -16,7 +16,9 @@ export async function smoke(context: InstallContext): Promise<void> {
await runVisible`test -s /etc/hy2xs/hy2xs.env`;
await runVisible`test -s /etc/hysteria/post-install.env`;
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
await runVisible`grep -q '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath}`;
await runVisible`grep -q '^ADMIN_USER=' ${context.config.bootstrapAdminSecretPath}`;
await runVisible`grep -q '^ADMIN_INITIAL_PASSWORD=' ${context.config.bootstrapAdminSecretPath}`;
await runVisible`grep -q '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath}`;
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`;
await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`;
@@ -32,7 +34,7 @@ export async function smoke(context: InstallContext): Promise<void> {
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
}
const adminConPass = (await runSecret`grep '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d: -f2-`).trim();
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
if (!adminConPass) {
throw new Error("admin connection password is empty in bootstrap secret file");
}
+2 -1
View File
@@ -24,7 +24,7 @@ export type RuntimeConfig = {
domain: string;
publicHost: string;
publicPort: number;
ipv6Enabled: false;
ipv6Enabled: boolean;
sshPort: number;
firewallEnabled: boolean;
firewallStagedApply: boolean;
@@ -34,6 +34,7 @@ export type RuntimeConfig = {
adminInitialPassword: string;
adminConPass: string;
forcePasswordChange: boolean;
allowSelfSignedDev: boolean;
tlsMode: TlsMode;
acmeType: "http" | "tls" | "dns";
acmeEmail: string;