diff --git a/apps/controller/log.go b/apps/controller/log.go index 66f4585..efa6ca7 100644 --- a/apps/controller/log.go +++ b/apps/controller/log.go @@ -23,7 +23,7 @@ func LogSystem(c *gin.Context) { return } numLine := 0 - if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 { + if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 { numLine = *logSystemDto.NumLine } logLines, total, err := util.ReadLinesFromBottom(constant.SystemLogPath, numLine) @@ -62,7 +62,7 @@ func LogHysteria2(c *gin.Context) { return } numLine := 0 - if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 { + if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 { numLine = *logSystemDto.NumLine } logLines, total, err := util.ReadLinesFromBottom(constant.Hysteria2LogPath, numLine) diff --git a/apps/proxy/process.go b/apps/proxy/process.go deleted file mode 100644 index d2b27f1..0000000 --- a/apps/proxy/process.go +++ /dev/null @@ -1,229 +0,0 @@ -package proxy - -import ( - "bufio" - "errors" - "fmt" - "github.com/sirupsen/logrus" - "gopkg.in/natefinch/lumberjack.v2" - "hy2xs-admin/model/constant" - "io" - "os/exec" - "sync" - "syscall" - "time" -) - -var logger logrus.Logger - -func initLogger() { - logger.SetOutput(&lumberjack.Logger{ - Filename: constant.Hysteria2LogPath, - MaxSize: 1, - MaxBackups: 2, - MaxAge: 30, - Compress: true, - LocalTime: true, - }) - logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"}) - logger.SetLevel(logrus.InfoLevel) -} - -func init() { - initLogger() -} - -type process struct { - mutex *sync.Mutex - cmd *exec.Cmd -} - -func (p *process) isRunning() bool { - return p.cmd != nil && p.cmd.Process != nil && p.cmd.ProcessState == nil -} - -func (p *process) start(name string, arg ...string) error { - if !p.mutex.TryLock() { - logrus.Errorf("start cmd err: lock not acquired") - return errors.New("start cmd err") - } - defer p.mutex.Unlock() - - if p.isRunning() { - return nil - } - - cmd := exec.Command(name, arg...) - if cmd.Err != nil { - logrus.Errorf("cmd err: %v", cmd.Err) - return errors.New("cmd err") - } - - // Получение stdout и stderr команды - stdout, err := cmd.StdoutPipe() - if err != nil { - logrus.Errorf("Error obtaining stdout: %v", err) - return err - } - stderr, err := cmd.StderrPipe() - if err != nil { - logrus.Errorf("Error obtaining stderr: %v", err) - return err - } - - if err := cmd.Start(); err != nil { - logrus.Errorf("cmd start err: %v", err) - return errors.New("cmd start err") - } - - p.cmd = cmd - - go p.handleLogs(stdout, stderr) - - return nil -} - -func (p *process) stop() error { - if !p.mutex.TryLock() { - return errors.New("cmd stop err: lock not acquired") - } - - if !p.isRunning() { - p.mutex.Unlock() - return nil - } - - cmd := p.cmd - p.mutex.Unlock() - - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - - if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { - logrus.Warnf("send SIGTERM failed: %v", err) - } - - timer := time.NewTimer(3 * time.Second) - defer timer.Stop() - - select { - case err := <-done: - if normalizeExitErr(err, syscall.SIGTERM) != nil { - return fmt.Errorf("process exit failed: %w", err) - } - - case <-timer.C: - if err := cmd.Process.Kill(); err != nil { - return fmt.Errorf("SIGKILL failed: %w", err) - } - - err := <-done - if normalizeExitErr(err, syscall.SIGKILL) != nil { - return fmt.Errorf("process killed but exit abnormal: %w", err) - } - } - - p.mutex.Lock() - p.cmd = nil - p.mutex.Unlock() - - return nil -} - -func normalizeExitErr(err error, allowedSignals ...syscall.Signal) error { - if err == nil { - return nil - } - - exitErr, ok := err.(*exec.ExitError) - if !ok { - return err - } - - status, ok := exitErr.Sys().(syscall.WaitStatus) - if !ok { - return err - } - - if status.Signaled() { - sig := status.Signal() - for _, s := range allowedSignals { - if sig == s { - return nil - } - } - } - - return err -} - -func (p *process) release() error { - if !p.mutex.TryLock() { - logrus.Errorf("cmd release err: lock not acquired") - return errors.New("cmd release err") - } - defer p.mutex.Unlock() - - if !p.isRunning() { - return nil - } - - if err := p.cmd.Process.Release(); err != nil { - logrus.Errorf("cmd release err: %v", err) - return errors.New("cmd release err") - } - p.cmd = nil - return nil -} - -func (p *process) handleLogs(stdout, stderr io.ReadCloser) { - // Логи - stdoutChan := make(chan string) - stderrChan := make(chan string) - - go func() { - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - stdoutChan <- scanner.Text() - } - if err := scanner.Err(); err != nil { - logrus.Errorf("Error reading stdout: %v", err) - } - close(stdoutChan) - }() - - go func() { - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - stderrChan <- scanner.Text() - } - if err := scanner.Err(); err != nil { - logrus.Errorf("Error reading stderr: %v", err) - } - close(stderrChan) - }() - - for { - select { - case line, ok := <-stdoutChan: - if !ok { - stdoutChan = nil - } else { - logger.Infof(line) - } - case line, ok := <-stderrChan: - if !ok { - stderrChan = nil - } else { - logger.Errorf(line) - } - } - - // Когда оба channel закрыты, выходим из цикла - if stdoutChan == nil && stderrChan == nil { - break - } - } -} diff --git a/docs/07-systemd-and-firewall.md b/docs/07-systemd-and-firewall.md index f2c2962..06abf5e 100644 --- a/docs/07-systemd-and-firewall.md +++ b/docs/07-systemd-and-firewall.md @@ -29,10 +29,20 @@ Рекомендуемый hardening: - `NoNewPrivileges=true` - `PrivateTmp=true` +- `UMask=0077` - `ProtectHome=true` - `ProtectSystem=strict` +- `ReadOnlyPaths=/etc/hysteria/config.yaml` - `ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs` - `RestrictAddressFamilies=AF_INET AF_UNIX` +- `SystemCallArchitectures=native` +- `LockPersonality=true` + +Для `hysteria-server.service` также обязателен sandbox-контур: +- `ProtectSystem=strict` +- `ReadOnlyPaths=/etc/hysteria/config.yaml` +- `ReadWritePaths=/var/lib/hysteria` +- `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` Важно: - HY2XS admin не должен запускаться как часть unit Hysteria @@ -48,7 +58,23 @@ IPv4-only policy: - использовать `table ip`, а не `table inet`; - IPv6 правила не добавлять; -- UI-порт разрешать только с локального bind-host. +- UI работает только на `127.0.0.1` в production baseline. + +## Firewall modes + +`HY2XS_FIREWALL_ENABLED=true`: +- orchestrator управляет baseline nftables. + +`HY2XS_FIREWALL_ALLOW_TAKEOVER=false`: +- safe default. +- существующий не-HY2XS `/etc/nftables.conf` блокирует install/reconfigure (fail-fast). + +`HY2XS_FIREWALL_ALLOW_TAKEOVER=true`: +- явный destructive takeover. +- использовать только после ручной проверки хоста. + +`HY2XS_FIREWALL_ENABLED=false` или `--skip-firewall`: +- оператор полностью управляет firewall вручную. После staged-проверки можно включать default policy `drop`. diff --git a/docs/09-post-install-env.md b/docs/09-post-install-env.md index d03581a..2cac385 100644 --- a/docs/09-post-install-env.md +++ b/docs/09-post-install-env.md @@ -39,13 +39,15 @@ /etc/hysteria/post-install.env ``` -Оба файла должны иметь права `0600`. +`/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` +- `DEPLOY_TIMESTAMP` (last apply timestamp) - `PACKAGE_NAME` - `PACKAGE_BUILD_ID` - `PACKAGE_VERSION` diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index a9ac934..cbf61d8 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -25,7 +25,7 @@ 3. bundled HY2XS admin раскладывается локально из пакета 4. создаются нужные каталоги 5. создаются systemd unit-файлы -6. создаются `hy2xs.env` и `post-install.env` с правами `0600` +6. создаются `hy2xs.env` и `post-install.env` с правами `0600 root:root` 7. baseline firewall применяется корректно через staged mode 8. SSH остаётся доступным 9. `reconfigure --dry-run` выводит план изменений @@ -53,6 +53,8 @@ 18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin` 19. остановка/рестарт UI не останавливает `hysteria-server` 20. traffic accounting/kick ориентируются на systemd status, а не на SQLite `HYSTERIA2_ENABLE` +21. `/etc/hysteria/config.yaml` имеет `0640 hysteria:hy2xs-admin` +22. `hy2xs-admin` может читать `/etc/hysteria/config.yaml`, но не может писать ## D. Negative tests diff --git a/docs/12-operations-and-troubleshooting.md b/docs/12-operations-and-troubleshooting.md index 8e59637..ce0dfa3 100644 --- a/docs/12-operations-and-troubleshooting.md +++ b/docs/12-operations-and-troubleshooting.md @@ -51,6 +51,30 @@ nft list ruleset cat /etc/hysteria/post-install.env ``` +Проверка логов через journald: +```bash +journalctl -u hysteria-server -n 100 --no-pager +journalctl -u hy2xs-admin -n 100 --no-pager +``` + +## Auth endpoint fail checklist + +```bash +systemctl status hysteria-server +systemctl status hy2xs-admin + +sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml + +curl -sS -X POST \ + -H 'Content-Type: application/json' \ + --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' \ + http://127.0.0.1:8080/hui/hysteria2/auth + +curl -sS \ + -H "Authorization: " \ + http://127.0.0.1:36712/online +``` + ## Типовые проблемы ### Сервер установился, но UI не работает diff --git a/orchestrator/src/commands/install.ts b/orchestrator/src/commands/install.ts index 7ae7508..b786256 100644 --- a/orchestrator/src/commands/install.ts +++ b/orchestrator/src/commands/install.ts @@ -3,6 +3,7 @@ import type { InstallContext, InstallOptions } from "../types/context"; import { exists, readText, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; import { step } from "../lib/log"; +import { readPackageValue } from "../lib/packageMeta"; import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env"; import { preflight } from "../steps/preflight"; import { installDeps } from "../steps/deps"; @@ -15,14 +16,6 @@ import { applyFirewall } from "../steps/firewall"; import { writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; -async function readPackageValue(packageDir: string, file: string, fallback: string): Promise { - try { - return (await readText(`${packageDir}/metadata/${file}`)).trim(); - } catch { - return fallback; - } -} - function secret(): string { return randomBytes(24).toString("base64url"); } diff --git a/orchestrator/src/commands/reconfigure.ts b/orchestrator/src/commands/reconfigure.ts index 7dac97b..38cd9d3 100644 --- a/orchestrator/src/commands/reconfigure.ts +++ b/orchestrator/src/commands/reconfigure.ts @@ -9,6 +9,7 @@ import { applyFirewall } from "../steps/firewall"; import { writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; import { runVisible } from "../lib/process"; +import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta"; async function backupCurrentState(): Promise { await runVisible`mkdir -p /etc/hy2xs/backups`; @@ -48,11 +49,11 @@ export async function reconfigure(options: ReconfigureOptions): Promise { const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = { options, config, - packageVersion: "reconfigure", - packageBuildId: "reconfigure", + packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), + packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), hysteriaAuthPassword: "managed-by-ui-auth", - hysteriaVersion: "unknown" + hysteriaVersion: await readInstalledHysteriaVersion() }; step("preflight"); diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts index 5e67a90..1029ba1 100644 --- a/orchestrator/src/config/env.ts +++ b/orchestrator/src/config/env.ts @@ -115,7 +115,9 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { 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), + firewallAllowTakeover: parseBool("HY2XS_FIREWALL_ALLOW_TAKEOVER", env.HY2XS_FIREWALL_ALLOW_TAKEOVER, false), uiBindHost, + uiPublicAccess: parseBool("HY2XS_UI_PUBLIC_ACCESS", env.HY2XS_UI_PUBLIC_ACCESS, false), uiPort, adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"), adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD), @@ -146,7 +148,8 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { false ), hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml", - hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "latest", + allowLatestHysteria: parseBool("HY2XS_ALLOW_LATEST_HYSTERIA", env.HY2XS_ALLOW_LATEST_HYSTERIA, false), + hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "v2.6.0", installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin", dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin", logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs", @@ -170,6 +173,12 @@ export function validateRuntimeConfig(config: RuntimeConfig): void { if (config.hysteriaBindHost !== "0.0.0.0") { throw new Error("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 in production profile"); } + if (!config.uiPublicAccess && config.uiBindHost !== "127.0.0.1") { + throw new Error("HY2XS UI must bind to 127.0.0.1 in production baseline"); + } + if (config.hysteriaConfigPath !== "/etc/hysteria/config.yaml") { + throw new Error("HY2XS_HYSTERIA_CONFIG_PATH is fixed in production baseline: /etc/hysteria/config.yaml"); + } if (config.tlsMode === "acme") { if (!config.domain) { @@ -205,7 +214,9 @@ export function renderRuntimeEnv(config: RuntimeConfig): string { `HY2XS_SSH_PORT=${config.sshPort}`, `HY2XS_FIREWALL_ENABLED=${config.firewallEnabled}`, `HY2XS_FIREWALL_STAGED_APPLY=${config.firewallStagedApply}`, + `HY2XS_FIREWALL_ALLOW_TAKEOVER=${config.firewallAllowTakeover}`, `HY2XS_UI_BIND_HOST=${config.uiBindHost}`, + `HY2XS_UI_PUBLIC_ACCESS=${config.uiPublicAccess}`, `HY2XS_UI_PORT=${config.uiPort}`, `HY2XS_ADMIN_USER=${config.adminUser}`, `HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`, @@ -229,6 +240,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string { `HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`, `HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`, `HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`, + `HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`, `HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`, `HY2XS_INSTALL_DIR=${config.installDir}`, `HY2XS_DATA_DIR=${config.dataDir}`, diff --git a/orchestrator/src/lib/packageMeta.ts b/orchestrator/src/lib/packageMeta.ts new file mode 100644 index 0000000..340a0c8 --- /dev/null +++ b/orchestrator/src/lib/packageMeta.ts @@ -0,0 +1,20 @@ +import { readText } from "./fs"; +import { run } from "./process"; + +export async function readPackageValue(packageDir: string, file: string, fallback: string): Promise { + try { + return (await readText(`${packageDir}/metadata/${file}`)).trim(); + } catch { + return fallback; + } +} + +export async function readInstalledHysteriaVersion(): Promise { + try { + const raw = await run`/usr/local/bin/hysteria version`; + const match = raw.match(/v\d+\.\d+\.\d+/); + return match ? match[0] : raw.trim(); + } catch { + return "unknown"; + } +} diff --git a/orchestrator/src/steps/config.ts b/orchestrator/src/steps/config.ts index 339c16a..6987b97 100644 --- a/orchestrator/src/steps/config.ts +++ b/orchestrator/src/steps/config.ts @@ -28,15 +28,21 @@ export async function generateConfig(context: InstallContext): Promise { 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}`; } diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index cad3d5b..63169ee 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -5,7 +5,7 @@ export async function writePostInstallEnv(context: InstallContext): Promise 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 { if (context.options.skipFirewall || !context.config.firewallEnabled) { info("firewall skipped by flag"); @@ -17,11 +41,17 @@ export async function applyFirewall(context: InstallContext): Promise { 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 { 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" diff --git a/orchestrator/src/steps/hysteria.ts b/orchestrator/src/steps/hysteria.ts index 72afb20..b956097 100644 --- a/orchestrator/src/steps/hysteria.ts +++ b/orchestrator/src/steps/hysteria.ts @@ -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 { 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"; diff --git a/orchestrator/src/steps/smoke.ts b/orchestrator/src/steps/smoke.ts index 038d129..15f4d3d 100644 --- a/orchestrator/src/steps/smoke.ts +++ b/orchestrator/src/steps/smoke.ts @@ -19,10 +19,17 @@ export async function smoke(context: InstallContext): Promise { 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} '`; diff --git a/orchestrator/src/types/context.ts b/orchestrator/src/types/context.ts index 594030f..3cee1d0 100644 --- a/orchestrator/src/types/context.ts +++ b/orchestrator/src/types/context.ts @@ -28,7 +28,9 @@ export type RuntimeConfig = { sshPort: number; firewallEnabled: boolean; firewallStagedApply: boolean; + firewallAllowTakeover: boolean; uiBindHost: string; + uiPublicAccess: boolean; uiPort: number; adminUser: string; adminInitialPassword: string; @@ -52,6 +54,7 @@ export type RuntimeConfig = { hysteriaBandwidthDown: string; hysteriaIgnoreClientBandwidth: boolean; hysteriaConfigPath: string; + allowLatestHysteria: boolean; hysteriaVersionPolicy: string; installDir: string; dataDir: string; diff --git a/package/config/hy2xs.env b/package/config/hy2xs.env index 7b40692..23360d1 100644 --- a/package/config/hy2xs.env +++ b/package/config/hy2xs.env @@ -6,7 +6,9 @@ HY2XS_PUBLIC_PORT=443 HY2XS_SSH_PORT=22 HY2XS_FIREWALL_ENABLED=true HY2XS_FIREWALL_STAGED_APPLY=true +HY2XS_FIREWALL_ALLOW_TAKEOVER=false HY2XS_UI_BIND_HOST=127.0.0.1 +HY2XS_UI_PUBLIC_ACCESS=false HY2XS_UI_PORT=8080 HY2XS_ADMIN_USER=hy2xsadmin HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__ @@ -30,6 +32,7 @@ HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml +HY2XS_ALLOW_LATEST_HYSTERIA=false HY2XS_HYSTERIA_VERSION=v2.6.0 HY2XS_INSTALL_DIR=/opt/hy2xs-admin HY2XS_DATA_DIR=/var/lib/hy2xs-admin diff --git a/package/install.sh b/package/install.sh old mode 100644 new mode 100755 diff --git a/package/systemd/hy2xs-admin.service b/package/systemd/hy2xs-admin.service index 0bf2490..55ede3c 100644 --- a/package/systemd/hy2xs-admin.service +++ b/package/systemd/hy2xs-admin.service @@ -16,10 +16,14 @@ Restart=on-failure RestartSec=5s NoNewPrivileges=true PrivateTmp=true +UMask=0077 ProtectHome=true ProtectSystem=strict +ReadOnlyPaths=/etc/hysteria/config.yaml ReadWritePaths={{DATA_DIR}} {{LOG_DIR}} RestrictAddressFamilies=AF_INET AF_UNIX +SystemCallArchitectures=native +LockPersonality=true CapabilityBoundingSet= AmbientCapabilities= diff --git a/package/systemd/hysteria-server.service b/package/systemd/hysteria-server.service index bbcac94..2c6bb2a 100644 --- a/package/systemd/hysteria-server.service +++ b/package/systemd/hysteria-server.service @@ -11,7 +11,16 @@ ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/config.yaml Restart=on-failure RestartSec=5s AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=true +PrivateTmp=true +ProtectHome=true +ProtectSystem=strict +ReadOnlyPaths=/etc/hysteria/config.yaml +ReadWritePaths=/var/lib/hysteria +RestrictAddressFamilies=AF_INET AF_UNIX +SystemCallArchitectures=native +LockPersonality=true [Install] WantedBy=multi-user.target diff --git a/package/templates/env/post-install.env.tpl b/package/templates/env/post-install.env.tpl index 25d9d5b..1837b36 100644 --- a/package/templates/env/post-install.env.tpl +++ b/package/templates/env/post-install.env.tpl @@ -2,7 +2,7 @@ # Файл создаётся оркестратором после первичной установки и не является runtime-конфигом. DEPLOY_TARGET_OS=debian12 -DEPLOY_TIMESTAMP={{INSTALL_DATE}} +DEPLOY_TIMESTAMP={{LAST_APPLY_DATE}} PACKAGE_NAME=hy2xs-install-package PACKAGE_BUILD_ID={{PACKAGE_BUILD_ID}} PACKAGE_VERSION={{PACKAGE_VERSION}} diff --git a/package/templates/nftables/hy2xs.nft.tpl b/package/templates/nftables/hy2xs.nft.tpl index 3bd041c..7e18fe2 100644 --- a/package/templates/nftables/hy2xs.nft.tpl +++ b/package/templates/nftables/hy2xs.nft.tpl @@ -7,7 +7,6 @@ table ip hy2xs { tcp dport {{SSH_PORT}} accept {{ACME_RULE}} udp dport {{HYSTERIA_PORT}} accept - tcp dport {{UI_PORT}} ip saddr {{UI_BIND_HOST}} accept icmp type echo-request accept } } diff --git a/tools/build/build.sh b/tools/build/build.sh old mode 100644 new mode 100755 diff --git a/tools/build/lib/package.sh b/tools/build/lib/package.sh index dbd809a..9709bbe 100644 --- a/tools/build/lib/package.sh +++ b/tools/build/lib/package.sh @@ -11,6 +11,11 @@ prepare_stage() { rm -rf "$STAGE_DIR" mkdir -p "$STAGE_DIR" "dist" copy_dir_contents "package" "$STAGE_DIR" + chmod 0755 "$STAGE_DIR/install.sh" + find "$STAGE_DIR" -type d -exec chmod 0755 {} + + find "$STAGE_DIR" -type f -name "*.service" -exec chmod 0644 {} + + find "$STAGE_DIR" -type f -name "*.tpl" -exec chmod 0644 {} + + find "$STAGE_DIR/config" -type f -exec chmod 0644 {} + rm -rf "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui" "$STAGE_DIR/metadata" mkdir -p "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui/hy2xs-admin" "$STAGE_DIR/metadata" @@ -87,5 +92,5 @@ create_archive() { local archive="dist/hy2xs-install-${version}.tar.gz" rm -f "$archive" - tar -C "tools/build/output" -czf "$archive" "hy2xs-install" + tar --owner=0 --group=0 --numeric-owner -C "tools/build/output" -czf "$archive" "hy2xs-install" } diff --git a/tools/build/lib/verify.sh b/tools/build/lib/verify.sh index 3cba1d8..4406a78 100644 --- a/tools/build/lib/verify.sh +++ b/tools/build/lib/verify.sh @@ -40,4 +40,12 @@ verify_archive() { env_content="$(tar -xOzf "$archive" hy2xs-install/config/hy2xs.env)" printf '%s\n' "$env_content" | grep -q 'replace-with-your-domain.example' && fail "packaged hy2xs.env contains placeholder domain" printf '%s\n' "$env_content" | grep -q 'replace-with-your-email@example.com' && fail "packaged hy2xs.env contains placeholder email" + + local tmp + tmp="$(mktemp -d)" + tar -xzf "$archive" -C "$tmp" + [ -x "$tmp/hy2xs-install/install.sh" ] || fail "install.sh is not executable" + [ -x "$tmp/hy2xs-install/orchestrator/hy2xs-orchestrator" ] || fail "orchestrator is not executable" + [ -x "$tmp/hy2xs-install/ui/hy2xs-admin/hy2xs-admin" ] || fail "hy2xs-admin is not executable" + rm -rf "$tmp" }