Полный продовый фикс fix22: DNS preflight, inet firewall, idempotent bootstrap, auth-semantics и redact-config
This commit is contained in:
@@ -11,11 +11,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Hysteria2Auth(c *gin.Context) {
|
func Hysteria2Auth(c *gin.Context) {
|
||||||
hysteria2AuthDto, err := validateField(c, dto.Hysteria2AuthDto{})
|
var req dto.Hysteria2AuthDto
|
||||||
if err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
vo.Hysteria2AuthBadRequest(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, username, err := service.Hysteria2Auth(*hysteria2AuthDto.Auth)
|
if req.Addr == nil || req.Auth == nil || req.Tx == nil {
|
||||||
|
vo.Hysteria2AuthBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, username, err := service.Hysteria2Auth(*req.Auth)
|
||||||
if err != nil || username == "" {
|
if err != nil || username == "" {
|
||||||
vo.Hysteria2AuthFail("", c)
|
vo.Hysteria2AuthFail("", c)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ func Hysteria2AuthFail(id string, c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Hysteria2AuthBadRequest(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusBadRequest, hysteria2Result{
|
||||||
|
Ok: false,
|
||||||
|
Id: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type Hysteria2SubscribeVo struct {
|
type Hysteria2SubscribeVo struct {
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
QrCode []byte `json:"qrCode"`
|
QrCode []byte `json:"qrCode"`
|
||||||
|
|||||||
@@ -91,3 +91,18 @@ hy2xs-orchestrator doctor --package-dir /usr/local/lib/hy2xs/package --config /e
|
|||||||
|
|
||||||
Команда выполняет preflight + smoke как post-install/post-reboot validation.
|
Команда выполняет preflight + smoke как post-install/post-reboot validation.
|
||||||
|
|
||||||
|
## 13. Secret-safe config sharing
|
||||||
|
|
||||||
|
Для передачи конфигов в тикеты/чаты используйте встроенную redaction-команду:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hy2xs-orchestrator redact-config --config /etc/hy2xs/hy2xs.env --out /root/hy2xs.redacted.env
|
||||||
|
hy2xs-orchestrator redact-config --config /etc/hysteria/post-install.env --out /root/post-install.redacted.env
|
||||||
|
hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/hysteria-config.redacted.yaml --format yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Инварианты:
|
||||||
|
- команда не выводит исходные секреты в stdout;
|
||||||
|
- требуется выбрать ровно один режим: `--in-place` или `--out <path>`;
|
||||||
|
- `--format auto` пытается определить формат по имени файла, при неоднозначности используйте `--format env|yaml`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { reconfigure, repair } from "./commands/reconfigure";
|
|||||||
import { doctor } from "./commands/doctor";
|
import { doctor } from "./commands/doctor";
|
||||||
import { status } from "./commands/status";
|
import { status } from "./commands/status";
|
||||||
import { diagnosticsCollect } from "./commands/diagnostics";
|
import { diagnosticsCollect } from "./commands/diagnostics";
|
||||||
|
import { redactConfig, type RedactConfigFormat } from "./commands/redact-config";
|
||||||
import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
||||||
|
|
||||||
function usage(): never {
|
function usage(): never {
|
||||||
@@ -13,10 +14,65 @@ function usage(): never {
|
|||||||
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||||
console.error(" hy2xs-orchestrator status --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
console.error(" hy2xs-orchestrator status --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||||
console.error(" hy2xs-orchestrator diagnostics collect --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
console.error(" hy2xs-orchestrator diagnostics collect --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||||
|
console.error(" hy2xs-orchestrator redact-config --config <path> [--in-place | --out <path>] [--format auto|env|yaml]");
|
||||||
console.error(" note: --skip-start is deprecated alias for --skip-service-start --skip-smoke");
|
console.error(" note: --skip-start is deprecated alias for --skip-service-start --skip-smoke");
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseRedactConfigOptions(args: string[]): {
|
||||||
|
configPath: string;
|
||||||
|
outPath: string;
|
||||||
|
inPlace: boolean;
|
||||||
|
format: RedactConfigFormat;
|
||||||
|
} {
|
||||||
|
let configPath = "";
|
||||||
|
let outPath = "";
|
||||||
|
let inPlace = false;
|
||||||
|
let format: RedactConfigFormat = "auto";
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i += 1) {
|
||||||
|
const arg = args[i];
|
||||||
|
switch (arg) {
|
||||||
|
case "--config":
|
||||||
|
configPath = takeValue(args, i, arg);
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
case "--out":
|
||||||
|
outPath = takeValue(args, i, arg);
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
case "--in-place":
|
||||||
|
inPlace = true;
|
||||||
|
break;
|
||||||
|
case "--format": {
|
||||||
|
const value = takeValue(args, i, arg);
|
||||||
|
if (value !== "auto" && value !== "env" && value !== "yaml") {
|
||||||
|
console.error(`invalid --format value: ${value}`);
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
format = value;
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.error(`Unknown argument: ${arg}`);
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configPath) {
|
||||||
|
console.error("Missing --config");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inPlace === (outPath !== "")) {
|
||||||
|
console.error("Specify exactly one of --in-place or --out <path>");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { configPath, outPath, inPlace, format };
|
||||||
|
}
|
||||||
|
|
||||||
function takeValue(args: string[], index: number, flag: string): string {
|
function takeValue(args: string[], index: number, flag: string): string {
|
||||||
const value = args[index + 1];
|
const value = args[index + 1];
|
||||||
if (!value || value.startsWith("--")) {
|
if (!value || value.startsWith("--")) {
|
||||||
@@ -76,6 +132,11 @@ function parseInstallOptions(args: string[]): InstallOptions {
|
|||||||
usage();
|
usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.skipSmoke && process.env.HY2XS_BREAK_GLASS !== "1") {
|
||||||
|
console.error("--skip-smoke is break-glass only. Set HY2XS_BREAK_GLASS=1 to continue.");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +200,11 @@ function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
|||||||
usage();
|
usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.skipSmoke && process.env.HY2XS_BREAK_GLASS !== "1") {
|
||||||
|
console.error("--skip-smoke is break-glass only. Set HY2XS_BREAK_GLASS=1 to continue.");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +244,10 @@ async function main(): Promise<void> {
|
|||||||
await diagnosticsCollect(parseCommonOptions(rest));
|
await diagnosticsCollect(parseCommonOptions(rest));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command === "redact-config") {
|
||||||
|
await redactConfig(parseRedactConfigOptions(args));
|
||||||
|
return;
|
||||||
|
}
|
||||||
usage();
|
usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
import type { CommonOptions } from "../types/context";
|
import type { CommonOptions } from "../types/context";
|
||||||
import { info, setOperationContext } from "../lib/log";
|
import { info, setOperationContext } from "../lib/log";
|
||||||
import { run } from "../lib/process";
|
import { run } from "../lib/process";
|
||||||
|
import { redactEnv, redactYaml } from "../lib/redaction";
|
||||||
|
|
||||||
function shellEscapeSingleQuotes(value: string): string {
|
function shellEscapeSingleQuotes(value: string): string {
|
||||||
return value.replaceAll("'", "'\\''");
|
return value.replaceAll("'", "'\\''");
|
||||||
}
|
}
|
||||||
|
|
||||||
function redactEnv(content: string): string {
|
|
||||||
return content
|
|
||||||
.replace(/^(HY2XS_ADMIN_INITIAL_PASSWORD=).*$/gm, "$1<redacted>")
|
|
||||||
.replace(/^(HY2XS_ADMIN_CON_PASS=).*$/gm, "$1<redacted>")
|
|
||||||
.replace(/^(HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=).*$/gm, "$1<redacted>")
|
|
||||||
.replace(/^(HY2XS_HYSTERIA_OBFS_PASSWORD=).*$/gm, "$1<redacted>");
|
|
||||||
}
|
|
||||||
|
|
||||||
function redactYaml(content: string): string {
|
|
||||||
return content
|
|
||||||
.replace(/(password:\s*).*/gi, "$1<redacted>")
|
|
||||||
.replace(/(secret:\s*).*/gi, "$1<redacted>")
|
|
||||||
.replace(/(auth:\s*).*/gi, "$1<redacted>");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function diagnosticsCollect(_options: CommonOptions): Promise<void> {
|
export async function diagnosticsCollect(_options: CommonOptions): Promise<void> {
|
||||||
const opId = `diag-${Date.now().toString(36)}`;
|
const opId = `diag-${Date.now().toString(36)}`;
|
||||||
setOperationContext(opId);
|
setOperationContext(opId);
|
||||||
@@ -49,6 +35,13 @@ export async function diagnosticsCollect(_options: CommonOptions): Promise<void>
|
|||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postInstallRaw = await Bun.file(`${outDir}/post-install.env`).text();
|
||||||
|
await Bun.write(`${outDir}/post-install.env`, redactEnv(postInstallRaw));
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfgRaw = await Bun.file(`${outDir}/hysteria-config.yaml`).text();
|
const cfgRaw = await Bun.file(`${outDir}/hysteria-config.yaml`).text();
|
||||||
await Bun.write(`${outDir}/hysteria-config.yaml`, redactYaml(cfgRaw));
|
await Bun.write(`${outDir}/hysteria-config.yaml`, redactYaml(cfgRaw));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { installHysteria } from "../steps/hysteria";
|
|||||||
import { generateConfig } from "../steps/config";
|
import { generateConfig } from "../steps/config";
|
||||||
import { deploySystemd } from "../steps/systemd";
|
import { deploySystemd } from "../steps/systemd";
|
||||||
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
|
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
|
||||||
import { writeBootstrapAdminSecret, writePostInstallEnv } from "../steps/env";
|
import { ensureBootstrapAdminSecret, writePostInstallEnv } from "../steps/env";
|
||||||
import { smoke } from "../steps/smoke";
|
import { smoke } from "../steps/smoke";
|
||||||
import { diagnosticsCollect } from "./diagnostics";
|
import { diagnosticsCollect } from "./diagnostics";
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ type InstallPhase =
|
|||||||
| "postinstall_env_written"
|
| "postinstall_env_written"
|
||||||
| "bootstrap_secret_written"
|
| "bootstrap_secret_written"
|
||||||
| "services_started"
|
| "services_started"
|
||||||
|
| "smoke_running"
|
||||||
| "smoke_failed"
|
| "smoke_failed"
|
||||||
| "failed"
|
| "failed"
|
||||||
| "installed";
|
| "installed";
|
||||||
@@ -232,15 +233,15 @@ export async function install(options: InstallOptions): Promise<void> {
|
|||||||
await advanceInstallState(context, "postinstall_env_written");
|
await advanceInstallState(context, "postinstall_env_written");
|
||||||
state.lastPhase = "postinstall_env_written";
|
state.lastPhase = "postinstall_env_written";
|
||||||
step("bootstrap admin secret");
|
step("bootstrap admin secret");
|
||||||
await writeBootstrapAdminSecret(context);
|
await ensureBootstrapAdminSecret(context);
|
||||||
stepDone("bootstrap admin secret");
|
stepDone("bootstrap admin secret");
|
||||||
await advanceInstallState(context, "bootstrap_secret_written");
|
await advanceInstallState(context, "bootstrap_secret_written");
|
||||||
state.lastPhase = "bootstrap_secret_written";
|
state.lastPhase = "bootstrap_secret_written";
|
||||||
step("smoke checks");
|
step("smoke checks");
|
||||||
await advanceInstallState(context, "services_started");
|
await advanceInstallState(context, "services_started");
|
||||||
state.lastPhase = "services_started";
|
state.lastPhase = "services_started";
|
||||||
await advanceInstallState(context, "smoke_failed");
|
await advanceInstallState(context, "smoke_running");
|
||||||
state.lastPhase = "smoke_failed";
|
state.lastPhase = "smoke_running";
|
||||||
await smoke(context);
|
await smoke(context);
|
||||||
stepDone("smoke checks");
|
stepDone("smoke checks");
|
||||||
step("finalize firewall rollback guard");
|
step("finalize firewall rollback guard");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { preflight } from "../steps/preflight";
|
|||||||
import { generateConfig } from "../steps/config";
|
import { generateConfig } from "../steps/config";
|
||||||
import { deploySystemd } from "../steps/systemd";
|
import { deploySystemd } from "../steps/systemd";
|
||||||
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
|
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
|
||||||
import { writePostInstallEnv } from "../steps/env";
|
import { ensureBootstrapAdminSecret, writePostInstallEnv } from "../steps/env";
|
||||||
import { smoke } from "../steps/smoke";
|
import { smoke } from "../steps/smoke";
|
||||||
import { runVisible } from "../lib/process";
|
import { runVisible } from "../lib/process";
|
||||||
import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta";
|
import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta";
|
||||||
@@ -219,6 +219,7 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||||
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
||||||
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
||||||
|
await ensureBootstrapAdminSecret(context);
|
||||||
await writePostInstallEnv(context);
|
await writePostInstallEnv(context);
|
||||||
stepDone("write env artifacts");
|
stepDone("write env artifacts");
|
||||||
await markPhase(context, "runtime_env_written");
|
await markPhase(context, "runtime_env_written");
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { fileExists, readText, writeTextAtomic } from "../lib/fs";
|
||||||
|
import { info, setOperationContext } from "../lib/log";
|
||||||
|
import { redactEnv, redactYaml } from "../lib/redaction";
|
||||||
|
|
||||||
|
export type RedactConfigFormat = "auto" | "env" | "yaml";
|
||||||
|
|
||||||
|
export type RedactConfigOptions = {
|
||||||
|
configPath: string;
|
||||||
|
outPath: string;
|
||||||
|
inPlace: boolean;
|
||||||
|
format: RedactConfigFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
function detectFormat(path: string): Exclude<RedactConfigFormat, "auto"> | null {
|
||||||
|
const p = path.toLowerCase();
|
||||||
|
if (p.endsWith(".env") || p.endsWith("hy2xs.env") || p.endsWith("post-install.env")) {
|
||||||
|
return "env";
|
||||||
|
}
|
||||||
|
if (p.endsWith(".yaml") || p.endsWith(".yml")) {
|
||||||
|
return "yaml";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFormat(format: RedactConfigFormat, path: string): Exclude<RedactConfigFormat, "auto"> {
|
||||||
|
if (format !== "auto") {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
const detected = detectFormat(path);
|
||||||
|
if (!detected) {
|
||||||
|
throw new Error(`unable to auto-detect format for ${path}; use --format env|yaml`);
|
||||||
|
}
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function redactConfig(options: RedactConfigOptions): Promise<void> {
|
||||||
|
setOperationContext(`redact-config-${Date.now().toString(36)}`);
|
||||||
|
|
||||||
|
if (!(await fileExists(options.configPath))) {
|
||||||
|
throw new Error(`config file not found: ${options.configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = options.inPlace ? options.configPath : options.outPath;
|
||||||
|
if (!targetPath) {
|
||||||
|
throw new Error("target path is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = await readText(options.configPath);
|
||||||
|
const format = resolveFormat(options.format, options.configPath);
|
||||||
|
const output = format === "env" ? redactEnv(input) : redactYaml(input);
|
||||||
|
|
||||||
|
await writeTextAtomic(targetPath, output, {
|
||||||
|
mode: 0o600,
|
||||||
|
owner: "root",
|
||||||
|
group: "root"
|
||||||
|
});
|
||||||
|
|
||||||
|
info(`redact-config done: source=${options.configPath} target=${targetPath} format=${format} in_place=${options.inPlace}`);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -53,20 +53,41 @@ export async function status(_options: CommonOptions): Promise<void> {
|
|||||||
|
|
||||||
const rollbackGuardUnits = (await run`sh -c 'systemctl list-units --all --no-legend "hy2xs-fw-rollback-*.timer" "hy2xs-fw-rollback-*.service" 2>/dev/null || true'`).trim();
|
const rollbackGuardUnits = (await run`sh -c 'systemctl list-units --all --no-legend "hy2xs-fw-rollback-*.timer" "hy2xs-fw-rollback-*.service" 2>/dev/null || true'`).trim();
|
||||||
|
|
||||||
|
const hysteriaService = await unitState("hysteria-server");
|
||||||
|
const adminService = await unitState("hy2xs-admin");
|
||||||
|
const firewall = await firewallState();
|
||||||
|
const installPhase = String(installState?.phase ?? "unknown");
|
||||||
|
const installStateEffective = installState?.installed
|
||||||
|
? "installed"
|
||||||
|
: (installPhase === "unknown" ? "failed" : installPhase);
|
||||||
|
const rollbackGuardActive = rollbackGuardUnits.length > 0;
|
||||||
|
const runtimeState = (hysteriaService === "active" && adminService === "active")
|
||||||
|
? (installStateEffective === "installed" ? "running" : "partial")
|
||||||
|
: "stopped";
|
||||||
|
const humanStatus = runtimeState === "partial"
|
||||||
|
? "Runtime services are active, but installation is not finalized because smoke checks failed."
|
||||||
|
: (runtimeState === "running"
|
||||||
|
? "Runtime services are active and installation is finalized."
|
||||||
|
: "Runtime services are not fully active.");
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
platform,
|
platform,
|
||||||
services: {
|
services: {
|
||||||
hysteria: await unitState("hysteria-server"),
|
hysteria: hysteriaService,
|
||||||
admin: await unitState("hy2xs-admin")
|
admin: adminService
|
||||||
},
|
},
|
||||||
firewall: await firewallState(),
|
firewall,
|
||||||
firewall_entrypoint_kind: await detectFirewallEntrypointKind(),
|
firewall_entrypoint_kind: await detectFirewallEntrypointKind(),
|
||||||
tls: await tlsState(),
|
tls: await tlsState(),
|
||||||
install_state_present: await fileExists(INSTALL_STATE_PATH),
|
install_state_present: await fileExists(INSTALL_STATE_PATH),
|
||||||
install_state: installState,
|
install_state: installState,
|
||||||
rollback_guard_active: rollbackGuardUnits.length > 0,
|
rollback_guard_active: rollbackGuardActive,
|
||||||
rollback_guard_units: rollbackGuardUnits ? rollbackGuardUnits.split("\n") : []
|
rollback_guard_units: rollbackGuardUnits ? rollbackGuardUnits.split("\n") : [],
|
||||||
|
runtime_state: runtimeState,
|
||||||
|
install_state_effective: installStateEffective,
|
||||||
|
firewall_state: rollbackGuardActive ? "guard_active" : firewall,
|
||||||
|
human_status: humanStatus
|
||||||
};
|
};
|
||||||
info(`status report: ${JSON.stringify(result)}`);
|
info(`status report: ${JSON.stringify(result)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export function redactKeyValueSecrets(content: string): string {
|
||||||
|
return content.replace(
|
||||||
|
/^([A-Z0-9_]*(PASSWORD|PASS|SECRET|TOKEN)[A-Z0-9_]*=).*$/gm,
|
||||||
|
"$1<redacted>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactEnv(content: string): string {
|
||||||
|
return redactKeyValueSecrets(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactYaml(content: string): string {
|
||||||
|
return content
|
||||||
|
.replace(/(password:\s*).*/gi, "$1<redacted>")
|
||||||
|
.replace(/(secret:\s*).*/gi, "$1<redacted>")
|
||||||
|
.replace(/(auth:\s*).*/gi, "$1<redacted>");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RuntimeContext } from "../types/context";
|
import type { RuntimeContext } from "../types/context";
|
||||||
import { readText, renderTemplate, writeTextAtomic } from "../lib/fs";
|
import { fileExists, readText, renderTemplate, writeTextAtomic } from "../lib/fs";
|
||||||
|
import { runVisible } from "../lib/process";
|
||||||
|
|
||||||
export async function writePostInstallEnv(context: RuntimeContext): Promise<void> {
|
export async function writePostInstallEnv(context: RuntimeContext): Promise<void> {
|
||||||
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
|
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
|
||||||
@@ -54,3 +55,17 @@ export async function writeBootstrapAdminSecret(context: RuntimeContext): Promis
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ensureBootstrapAdminSecret(context: RuntimeContext): Promise<void> {
|
||||||
|
const path = context.config.bootstrapAdminSecretPath;
|
||||||
|
if (!(await fileExists(path))) {
|
||||||
|
await writeBootstrapAdminSecret(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runVisible`test "$(stat -c '%U:%G' ${path})" = 'root:root'`;
|
||||||
|
await runVisible`test "$(stat -c '%a' ${path})" = '600'`;
|
||||||
|
await runVisible`grep -q '^ADMIN_USER=' ${path}`;
|
||||||
|
await runVisible`grep -q '^ADMIN_INITIAL_PASSWORD=' ${path}`;
|
||||||
|
await runVisible`grep -q '^ADMIN_CON_PASS=' ${path}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function applyFirewall(context: RuntimeContext): Promise<void> {
|
|||||||
|
|
||||||
const acmeChallengePort = context.config.acmeType === "tls" ? 443 : 80;
|
const acmeChallengePort = context.config.acmeType === "tls" ? 443 : 80;
|
||||||
const acmeRule = context.config.tlsMode === "acme"
|
const acmeRule = context.config.tlsMode === "acme"
|
||||||
? `tcp dport ${acmeChallengePort} accept`
|
? `meta nfproto ipv4 tcp dport ${acmeChallengePort} accept`
|
||||||
: "# acme challenge port disabled";
|
: "# acme challenge port disabled";
|
||||||
|
|
||||||
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
|
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import type { RuntimeContext } from "../types/context";
|
import type { RuntimeContext } from "../types/context";
|
||||||
|
import { resolve4, resolve6 } from "node:dns/promises";
|
||||||
import { dirExists, fileExists } from "../lib/fs";
|
import { dirExists, fileExists } from "../lib/fs";
|
||||||
import { fail, info } from "../lib/log";
|
import { fail, info } from "../lib/log";
|
||||||
import { run } from "../lib/process";
|
import { run } from "../lib/process";
|
||||||
import { assertPlatform } from "../platform/assert";
|
import { assertPlatform } from "../platform/assert";
|
||||||
|
|
||||||
|
function isNoDnsRecords(error: unknown): boolean {
|
||||||
|
return (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"code" in error &&
|
||||||
|
(((error as { code?: string }).code === "ENODATA") || ((error as { code?: string }).code === "ENOTFOUND"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function isTcpPortListening(port: number): Promise<boolean> {
|
async function isTcpPortListening(port: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const output = await run`ss -H -ltn`;
|
const output = await run`ss -H -ltn`;
|
||||||
@@ -114,24 +124,29 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.config.domain) {
|
if (context.config.domain) {
|
||||||
|
let a: string[] = [];
|
||||||
try {
|
try {
|
||||||
const a = await run`getent ahostsv4 ${context.config.domain}`;
|
a = await resolve4(context.config.domain);
|
||||||
if (!a.trim()) {
|
|
||||||
fail(`domain has no A-record: ${context.config.domain}`);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
fail(`domain has no A-record: ${context.config.domain}`);
|
fail(`domain has no A-record: ${context.config.domain}`);
|
||||||
}
|
}
|
||||||
|
if (a.length === 0) {
|
||||||
|
fail(`domain has no A-record: ${context.config.domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let aaaa: string[] = [];
|
||||||
try {
|
try {
|
||||||
const aaaa = await run`getent ahostsv6 ${context.config.domain}`;
|
aaaa = await resolve6(context.config.domain);
|
||||||
if (aaaa.trim()) {
|
} catch (error) {
|
||||||
|
if (!isNoDnsRecords(error)) {
|
||||||
|
fail(`DNS AAAA lookup failed for ${context.config.domain}: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aaaa.length > 0) {
|
||||||
fail(
|
fail(
|
||||||
`domain ${context.config.domain} has AAAA record while HY2XS profile is IPv4-only; remove AAAA record before install`
|
`domain ${context.config.domain} has DNS AAAA record while HY2XS profile is IPv4-only; remove AAAA record before install`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// no AAAA is acceptable
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hysteriaUdpBusy = await isUdpPortListening(context.config.hysteriaPort);
|
const hysteriaUdpBusy = await isUdpPortListening(context.config.hysteriaPort);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ HY2XS_DOMAIN=uk.api.withen.pro
|
|||||||
HY2XS_PUBLIC_HOST=uk.api.withen.pro
|
HY2XS_PUBLIC_HOST=uk.api.withen.pro
|
||||||
HY2XS_PUBLIC_PORT=443
|
HY2XS_PUBLIC_PORT=443
|
||||||
HY2XS_SSH_PORT=22
|
HY2XS_SSH_PORT=22
|
||||||
HY2XS_FIREWALL_MODE=managed
|
HY2XS_FIREWALL_MODE=takeover
|
||||||
HY2XS_FIREWALL_STAGED_APPLY=true
|
HY2XS_FIREWALL_STAGED_APPLY=true
|
||||||
HY2XS_UI_BIND_HOST=127.0.0.1
|
HY2XS_UI_BIND_HOST=127.0.0.1
|
||||||
HY2XS_UI_PUBLIC_ACCESS=false
|
HY2XS_UI_PUBLIC_ACCESS=false
|
||||||
@@ -12,7 +12,7 @@ HY2XS_UI_PORT=8080
|
|||||||
HY2XS_ADMIN_USER=hy2xsadmin
|
HY2XS_ADMIN_USER=hy2xsadmin
|
||||||
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
|
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
|
||||||
HY2XS_ADMIN_CON_PASS=__GENERATE__
|
HY2XS_ADMIN_CON_PASS=__GENERATE__
|
||||||
HY2XS_FORCE_PASSWORD_CHANGE=false
|
HY2XS_FORCE_PASSWORD_CHANGE=true
|
||||||
HY2XS_ALLOW_SELF_SIGNED_DEV=false
|
HY2XS_ALLOW_SELF_SIGNED_DEV=false
|
||||||
HY2XS_TLS_MODE=acme
|
HY2XS_TLS_MODE=acme
|
||||||
HY2XS_ACME_TYPE=http
|
HY2XS_ACME_TYPE=http
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
table ip hy2xs {
|
table inet hy2xs {
|
||||||
chain input {
|
chain input {
|
||||||
type filter hook input priority 0; policy drop;
|
type filter hook input priority 0; policy drop;
|
||||||
|
|
||||||
iif lo accept
|
iif lo accept
|
||||||
ct state established,related accept
|
ct state established,related accept
|
||||||
tcp dport {{SSH_PORT}} accept
|
meta nfproto ipv4 tcp dport {{SSH_PORT}} accept
|
||||||
{{ACME_RULE}}
|
{{ACME_RULE}}
|
||||||
udp dport {{HYSTERIA_PORT}} accept
|
meta nfproto ipv4 udp dport {{HYSTERIA_PORT}} accept
|
||||||
icmp type echo-request accept
|
meta nfproto ipv4 icmp type echo-request accept
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user