Production hardening: host-aware install, ownership contracts, firewall safety

This commit is contained in:
2026-04-30 03:26:14 +05:00
parent 0545990968
commit c10ab1fafd
25 changed files with 190 additions and 262 deletions
+9 -3
View File
@@ -28,15 +28,21 @@ export async function generateConfig(context: InstallContext): Promise<void> {
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
});
await writeText("/etc/hysteria/config.yaml.tmp", rendered, 0o600);
await runVisible`mv /etc/hysteria/config.yaml.tmp /etc/hysteria/config.yaml`;
const configPath = context.config.hysteriaConfigPath;
const tmpPath = `${configPath}.tmp`;
await runVisible`mkdir -p /etc/hysteria`;
await writeText(tmpPath, rendered, 0o600);
await runVisible`chown hysteria:hy2xs-admin ${tmpPath}`;
await runVisible`chmod 0640 ${tmpPath}`;
await runVisible`mv ${tmpPath} ${configPath}`;
if (context.config.tlsMode === "self_signed_dev") {
await runVisible`openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=${context.config.domain || "hy2xs.local"} -keyout ${context.config.tlsKeyPath} -out ${context.config.tlsCertPath}`;
await runVisible`chmod 600 ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
}
await runVisible`chown hysteria:hysteria /etc/hysteria/config.yaml`;
await runVisible`chown hysteria:hy2xs-admin ${configPath}`;
await runVisible`chmod 0640 ${configPath}`;
if (context.config.tlsMode !== "acme") {
await runVisible`chown hysteria:hysteria ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
}
+1 -1
View File
@@ -5,7 +5,7 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
PACKAGE_VERSION: context.packageVersion,
PACKAGE_BUILD_ID: context.packageBuildId,
INSTALL_DATE: context.installDate,
LAST_APPLY_DATE: context.installDate,
DOMAIN: context.config.domain,
PUBLIC_HOST: context.config.publicHost,
PUBLIC_PORT: context.config.publicPort,
+37 -4
View File
@@ -1,8 +1,32 @@
import type { InstallContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs";
import { info } from "../lib/log";
import { exists, readText, renderTemplate, writeText } from "../lib/fs";
import { fail, info } from "../lib/log";
import { runVisible } from "../lib/process";
function stripNftComments(content: string): string {
return content
.split(/\r?\n/)
.map((line) => line.replace(/#.*/, "").trim())
.filter(Boolean)
.join("\n");
}
function isSafeNftablesEntrypoint(content: string): boolean {
if (content.includes("HY2XS-MANAGED")) {
return true;
}
const effective = stripNftComments(content)
.replace(/^#!\/usr\/sbin\/nft\s+-f\s*/m, "")
.trim();
if (!effective) {
return true;
}
return effective === "flush ruleset";
}
export async function applyFirewall(context: InstallContext): Promise<void> {
if (context.options.skipFirewall || !context.config.firewallEnabled) {
info("firewall skipped by flag");
@@ -17,11 +41,17 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
SSH_PORT: context.config.sshPort,
HYSTERIA_PORT: context.config.hysteriaPort,
UI_PORT: context.config.uiPort,
UI_BIND_HOST: context.config.uiBindHost,
ACME_RULE: acmeRule
});
const existing = await exists("/etc/nftables.conf")
? await readText("/etc/nftables.conf")
: "";
if (existing && !isSafeNftablesEntrypoint(existing) && !context.config.firewallAllowTakeover) {
fail("existing non-HY2XS nftables.conf found; set HY2XS_FIREWALL_ALLOW_TAKEOVER=true or HY2XS_FIREWALL_ENABLED=false");
}
await runVisible`cp -a /etc/nftables.conf /etc/nftables.conf.hy2xs.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`;
await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > /etc/nftables.d/hy2xs.nft.existed || rm -f /etc/nftables.d/hy2xs.nft.existed`;
@@ -30,6 +60,9 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`;
const nftablesConf = `#!/usr/sbin/nft -f
# HY2XS-MANAGED: root nftables entrypoint
# Generated by hy2xs-orchestrator. Do not edit manually; edit /etc/hy2xs/hy2xs.env and run reconfigure.
flush ruleset
include "/etc/nftables.d/hy2xs.nft"
+3 -3
View File
@@ -10,9 +10,6 @@ function normalizeInstalledVersion(raw: string): string {
}
function validateVersionPolicy(value: string): void {
if (value === "latest") {
return;
}
if (/^v\d+\.\d+\.\d+$/.test(value)) {
return;
}
@@ -21,6 +18,9 @@ function validateVersionPolicy(value: string): void {
export async function installHysteria(context: InstallContext): Promise<void> {
const policy = context.config.hysteriaVersionPolicy;
if (policy === "latest" && !context.config.allowLatestHysteria) {
throw new Error("HY2XS_HYSTERIA_VERSION=latest is not allowed in production; pin vX.Y.Z or set HY2XS_ALLOW_LATEST_HYSTERIA=true");
}
validateVersionPolicy(policy);
const scriptPath = "/tmp/hy2xs-install-hysteria.sh";
+8 -1
View File
@@ -19,10 +19,17 @@ export async function smoke(context: InstallContext): Promise<void> {
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/hysteria/config.yaml)" = '640'`;
await runVisible`test "$(stat -c '%U:%G' /etc/hysteria/config.yaml)" = 'hysteria:hy2xs-admin'`;
await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`;
await runVisible`test "$(stat -c '%U:%G' /etc/hy2xs/hy2xs.env)" = 'root:root'`;
await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`;
await runVisible`test "$(stat -c '%U:%G' /etc/hysteria/post-install.env)" = 'root:root'`;
await runVisible`test "$(stat -c '%a' ${context.config.bootstrapAdminSecretPath})" = '600'`;
await runVisible`test "$(stat -c '%U:%G' ${context.config.bootstrapAdminSecretPath})" = 'root:root'`;
await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml`;
await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`;
if (context.config.uiBindHost === "127.0.0.1") {
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`;