Production hardening: host-aware install, ownership contracts, firewall safety
This commit is contained in:
@@ -23,7 +23,7 @@ func LogSystem(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
numLine := 0
|
numLine := 0
|
||||||
if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 {
|
if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 {
|
||||||
numLine = *logSystemDto.NumLine
|
numLine = *logSystemDto.NumLine
|
||||||
}
|
}
|
||||||
logLines, total, err := util.ReadLinesFromBottom(constant.SystemLogPath, numLine)
|
logLines, total, err := util.ReadLinesFromBottom(constant.SystemLogPath, numLine)
|
||||||
@@ -62,7 +62,7 @@ func LogHysteria2(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
numLine := 0
|
numLine := 0
|
||||||
if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 {
|
if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 {
|
||||||
numLine = *logSystemDto.NumLine
|
numLine = *logSystemDto.NumLine
|
||||||
}
|
}
|
||||||
logLines, total, err := util.ReadLinesFromBottom(constant.Hysteria2LogPath, numLine)
|
logLines, total, err := util.ReadLinesFromBottom(constant.Hysteria2LogPath, numLine)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,10 +29,20 @@
|
|||||||
Рекомендуемый hardening:
|
Рекомендуемый hardening:
|
||||||
- `NoNewPrivileges=true`
|
- `NoNewPrivileges=true`
|
||||||
- `PrivateTmp=true`
|
- `PrivateTmp=true`
|
||||||
|
- `UMask=0077`
|
||||||
- `ProtectHome=true`
|
- `ProtectHome=true`
|
||||||
- `ProtectSystem=strict`
|
- `ProtectSystem=strict`
|
||||||
|
- `ReadOnlyPaths=/etc/hysteria/config.yaml`
|
||||||
- `ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs`
|
- `ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs`
|
||||||
- `RestrictAddressFamilies=AF_INET AF_UNIX`
|
- `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
|
- HY2XS admin не должен запускаться как часть unit Hysteria
|
||||||
@@ -48,7 +58,23 @@
|
|||||||
IPv4-only policy:
|
IPv4-only policy:
|
||||||
- использовать `table ip`, а не `table inet`;
|
- использовать `table ip`, а не `table inet`;
|
||||||
- IPv6 правила не добавлять;
|
- 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`.
|
После staged-проверки можно включать default policy `drop`.
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,15 @@
|
|||||||
/etc/hysteria/post-install.env
|
/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 / package
|
||||||
- `DEPLOY_TARGET_OS`
|
- `DEPLOY_TARGET_OS`
|
||||||
- `DEPLOY_TIMESTAMP`
|
- `DEPLOY_TIMESTAMP` (last apply timestamp)
|
||||||
- `PACKAGE_NAME`
|
- `PACKAGE_NAME`
|
||||||
- `PACKAGE_BUILD_ID`
|
- `PACKAGE_BUILD_ID`
|
||||||
- `PACKAGE_VERSION`
|
- `PACKAGE_VERSION`
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
3. bundled HY2XS admin раскладывается локально из пакета
|
3. bundled HY2XS admin раскладывается локально из пакета
|
||||||
4. создаются нужные каталоги
|
4. создаются нужные каталоги
|
||||||
5. создаются systemd unit-файлы
|
5. создаются systemd unit-файлы
|
||||||
6. создаются `hy2xs.env` и `post-install.env` с правами `0600`
|
6. создаются `hy2xs.env` и `post-install.env` с правами `0600 root:root`
|
||||||
7. baseline firewall применяется корректно через staged mode
|
7. baseline firewall применяется корректно через staged mode
|
||||||
8. SSH остаётся доступным
|
8. SSH остаётся доступным
|
||||||
9. `reconfigure --dry-run` выводит план изменений
|
9. `reconfigure --dry-run` выводит план изменений
|
||||||
@@ -53,6 +53,8 @@
|
|||||||
18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin`
|
18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin`
|
||||||
19. остановка/рестарт UI не останавливает `hysteria-server`
|
19. остановка/рестарт UI не останавливает `hysteria-server`
|
||||||
20. traffic accounting/kick ориентируются на systemd status, а не на SQLite `HYSTERIA2_ENABLE`
|
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
|
## D. Negative tests
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,30 @@ nft list ruleset
|
|||||||
cat /etc/hysteria/post-install.env
|
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: <trafficStatsSecret>" \
|
||||||
|
http://127.0.0.1:36712/online
|
||||||
|
```
|
||||||
|
|
||||||
## Типовые проблемы
|
## Типовые проблемы
|
||||||
|
|
||||||
### Сервер установился, но UI не работает
|
### Сервер установился, но UI не работает
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { InstallContext, InstallOptions } from "../types/context";
|
|||||||
import { exists, readText, writeText } from "../lib/fs";
|
import { exists, readText, writeText } from "../lib/fs";
|
||||||
import { runVisible } from "../lib/process";
|
import { runVisible } from "../lib/process";
|
||||||
import { step } from "../lib/log";
|
import { step } from "../lib/log";
|
||||||
|
import { readPackageValue } from "../lib/packageMeta";
|
||||||
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
||||||
import { preflight } from "../steps/preflight";
|
import { preflight } from "../steps/preflight";
|
||||||
import { installDeps } from "../steps/deps";
|
import { installDeps } from "../steps/deps";
|
||||||
@@ -15,14 +16,6 @@ import { applyFirewall } from "../steps/firewall";
|
|||||||
import { writePostInstallEnv } from "../steps/env";
|
import { writePostInstallEnv } from "../steps/env";
|
||||||
import { smoke } from "../steps/smoke";
|
import { smoke } from "../steps/smoke";
|
||||||
|
|
||||||
async function readPackageValue(packageDir: string, file: string, fallback: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
return (await readText(`${packageDir}/metadata/${file}`)).trim();
|
|
||||||
} catch {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function secret(): string {
|
function secret(): string {
|
||||||
return randomBytes(24).toString("base64url");
|
return randomBytes(24).toString("base64url");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { applyFirewall } from "../steps/firewall";
|
|||||||
import { writePostInstallEnv } from "../steps/env";
|
import { 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";
|
||||||
|
|
||||||
async function backupCurrentState(): Promise<void> {
|
async function backupCurrentState(): Promise<void> {
|
||||||
await runVisible`mkdir -p /etc/hy2xs/backups`;
|
await runVisible`mkdir -p /etc/hy2xs/backups`;
|
||||||
@@ -48,11 +49,11 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
||||||
options,
|
options,
|
||||||
config,
|
config,
|
||||||
packageVersion: "reconfigure",
|
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
|
||||||
packageBuildId: "reconfigure",
|
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
|
||||||
installDate: new Date().toISOString(),
|
installDate: new Date().toISOString(),
|
||||||
hysteriaAuthPassword: "managed-by-ui-auth",
|
hysteriaAuthPassword: "managed-by-ui-auth",
|
||||||
hysteriaVersion: "unknown"
|
hysteriaVersion: await readInstalledHysteriaVersion()
|
||||||
};
|
};
|
||||||
|
|
||||||
step("preflight");
|
step("preflight");
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22),
|
sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22),
|
||||||
firewallEnabled: parseBool("HY2XS_FIREWALL_ENABLED", env.HY2XS_FIREWALL_ENABLED, true),
|
firewallEnabled: parseBool("HY2XS_FIREWALL_ENABLED", env.HY2XS_FIREWALL_ENABLED, true),
|
||||||
firewallStagedApply: parseBool("HY2XS_FIREWALL_STAGED_APPLY", env.HY2XS_FIREWALL_STAGED_APPLY, 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,
|
uiBindHost,
|
||||||
|
uiPublicAccess: parseBool("HY2XS_UI_PUBLIC_ACCESS", env.HY2XS_UI_PUBLIC_ACCESS, false),
|
||||||
uiPort,
|
uiPort,
|
||||||
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
|
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
|
||||||
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
|
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
|
||||||
@@ -146,7 +148,8 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
false
|
false
|
||||||
),
|
),
|
||||||
hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml",
|
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",
|
installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin",
|
||||||
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
|
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
|
||||||
logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs",
|
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") {
|
if (config.hysteriaBindHost !== "0.0.0.0") {
|
||||||
throw new Error("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 in production profile");
|
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.tlsMode === "acme") {
|
||||||
if (!config.domain) {
|
if (!config.domain) {
|
||||||
@@ -205,7 +214,9 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
|
|||||||
`HY2XS_SSH_PORT=${config.sshPort}`,
|
`HY2XS_SSH_PORT=${config.sshPort}`,
|
||||||
`HY2XS_FIREWALL_ENABLED=${config.firewallEnabled}`,
|
`HY2XS_FIREWALL_ENABLED=${config.firewallEnabled}`,
|
||||||
`HY2XS_FIREWALL_STAGED_APPLY=${config.firewallStagedApply}`,
|
`HY2XS_FIREWALL_STAGED_APPLY=${config.firewallStagedApply}`,
|
||||||
|
`HY2XS_FIREWALL_ALLOW_TAKEOVER=${config.firewallAllowTakeover}`,
|
||||||
`HY2XS_UI_BIND_HOST=${config.uiBindHost}`,
|
`HY2XS_UI_BIND_HOST=${config.uiBindHost}`,
|
||||||
|
`HY2XS_UI_PUBLIC_ACCESS=${config.uiPublicAccess}`,
|
||||||
`HY2XS_UI_PORT=${config.uiPort}`,
|
`HY2XS_UI_PORT=${config.uiPort}`,
|
||||||
`HY2XS_ADMIN_USER=${config.adminUser}`,
|
`HY2XS_ADMIN_USER=${config.adminUser}`,
|
||||||
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
|
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
|
||||||
@@ -229,6 +240,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
|
|||||||
`HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
|
`HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
|
||||||
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
|
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
|
||||||
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
|
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
|
||||||
|
`HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`,
|
||||||
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
|
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
|
||||||
`HY2XS_INSTALL_DIR=${config.installDir}`,
|
`HY2XS_INSTALL_DIR=${config.installDir}`,
|
||||||
`HY2XS_DATA_DIR=${config.dataDir}`,
|
`HY2XS_DATA_DIR=${config.dataDir}`,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { readText } from "./fs";
|
||||||
|
import { run } from "./process";
|
||||||
|
|
||||||
|
export async function readPackageValue(packageDir: string, file: string, fallback: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
return (await readText(`${packageDir}/metadata/${file}`)).trim();
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readInstalledHysteriaVersion(): Promise<string> {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,15 +28,21 @@ export async function generateConfig(context: InstallContext): Promise<void> {
|
|||||||
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
|
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeText("/etc/hysteria/config.yaml.tmp", rendered, 0o600);
|
const configPath = context.config.hysteriaConfigPath;
|
||||||
await runVisible`mv /etc/hysteria/config.yaml.tmp /etc/hysteria/config.yaml`;
|
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") {
|
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`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`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") {
|
if (context.config.tlsMode !== "acme") {
|
||||||
await runVisible`chown hysteria:hysteria ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
|
await runVisible`chown hysteria:hysteria ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`), {
|
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
|
||||||
PACKAGE_VERSION: context.packageVersion,
|
PACKAGE_VERSION: context.packageVersion,
|
||||||
PACKAGE_BUILD_ID: context.packageBuildId,
|
PACKAGE_BUILD_ID: context.packageBuildId,
|
||||||
INSTALL_DATE: context.installDate,
|
LAST_APPLY_DATE: context.installDate,
|
||||||
DOMAIN: context.config.domain,
|
DOMAIN: context.config.domain,
|
||||||
PUBLIC_HOST: context.config.publicHost,
|
PUBLIC_HOST: context.config.publicHost,
|
||||||
PUBLIC_PORT: context.config.publicPort,
|
PUBLIC_PORT: context.config.publicPort,
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
import type { InstallContext } from "../types/context";
|
import type { InstallContext } from "../types/context";
|
||||||
import { readText, renderTemplate, writeText } from "../lib/fs";
|
import { exists, readText, renderTemplate, writeText } from "../lib/fs";
|
||||||
import { info } from "../lib/log";
|
import { fail, info } from "../lib/log";
|
||||||
import { runVisible } from "../lib/process";
|
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> {
|
export async function applyFirewall(context: InstallContext): Promise<void> {
|
||||||
if (context.options.skipFirewall || !context.config.firewallEnabled) {
|
if (context.options.skipFirewall || !context.config.firewallEnabled) {
|
||||||
info("firewall skipped by flag");
|
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`), {
|
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
|
||||||
SSH_PORT: context.config.sshPort,
|
SSH_PORT: context.config.sshPort,
|
||||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||||
UI_PORT: context.config.uiPort,
|
|
||||||
UI_BIND_HOST: context.config.uiBindHost,
|
|
||||||
ACME_RULE: acmeRule
|
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.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`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`;
|
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`;
|
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`;
|
||||||
|
|
||||||
const nftablesConf = `#!/usr/sbin/nft -f
|
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
|
flush ruleset
|
||||||
|
|
||||||
include "/etc/nftables.d/hy2xs.nft"
|
include "/etc/nftables.d/hy2xs.nft"
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ function normalizeInstalledVersion(raw: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateVersionPolicy(value: string): void {
|
function validateVersionPolicy(value: string): void {
|
||||||
if (value === "latest") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (/^v\d+\.\d+\.\d+$/.test(value)) {
|
if (/^v\d+\.\d+\.\d+$/.test(value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -21,6 +18,9 @@ function validateVersionPolicy(value: string): void {
|
|||||||
|
|
||||||
export async function installHysteria(context: InstallContext): Promise<void> {
|
export async function installHysteria(context: InstallContext): Promise<void> {
|
||||||
const policy = context.config.hysteriaVersionPolicy;
|
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);
|
validateVersionPolicy(policy);
|
||||||
|
|
||||||
const scriptPath = "/tmp/hy2xs-install-hysteria.sh";
|
const scriptPath = "/tmp/hy2xs-install-hysteria.sh";
|
||||||
|
|||||||
@@ -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_USER=' ${context.config.bootstrapAdminSecretPath}`;
|
||||||
await runVisible`grep -q '^ADMIN_INITIAL_PASSWORD=' ${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`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 '%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 '%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 '%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} '`;
|
await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`;
|
||||||
if (context.config.uiBindHost === "127.0.0.1") {
|
if (context.config.uiBindHost === "127.0.0.1") {
|
||||||
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`;
|
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`;
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export type RuntimeConfig = {
|
|||||||
sshPort: number;
|
sshPort: number;
|
||||||
firewallEnabled: boolean;
|
firewallEnabled: boolean;
|
||||||
firewallStagedApply: boolean;
|
firewallStagedApply: boolean;
|
||||||
|
firewallAllowTakeover: boolean;
|
||||||
uiBindHost: string;
|
uiBindHost: string;
|
||||||
|
uiPublicAccess: boolean;
|
||||||
uiPort: number;
|
uiPort: number;
|
||||||
adminUser: string;
|
adminUser: string;
|
||||||
adminInitialPassword: string;
|
adminInitialPassword: string;
|
||||||
@@ -52,6 +54,7 @@ export type RuntimeConfig = {
|
|||||||
hysteriaBandwidthDown: string;
|
hysteriaBandwidthDown: string;
|
||||||
hysteriaIgnoreClientBandwidth: boolean;
|
hysteriaIgnoreClientBandwidth: boolean;
|
||||||
hysteriaConfigPath: string;
|
hysteriaConfigPath: string;
|
||||||
|
allowLatestHysteria: boolean;
|
||||||
hysteriaVersionPolicy: string;
|
hysteriaVersionPolicy: string;
|
||||||
installDir: string;
|
installDir: string;
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ HY2XS_PUBLIC_PORT=443
|
|||||||
HY2XS_SSH_PORT=22
|
HY2XS_SSH_PORT=22
|
||||||
HY2XS_FIREWALL_ENABLED=true
|
HY2XS_FIREWALL_ENABLED=true
|
||||||
HY2XS_FIREWALL_STAGED_APPLY=true
|
HY2XS_FIREWALL_STAGED_APPLY=true
|
||||||
|
HY2XS_FIREWALL_ALLOW_TAKEOVER=false
|
||||||
HY2XS_UI_BIND_HOST=127.0.0.1
|
HY2XS_UI_BIND_HOST=127.0.0.1
|
||||||
|
HY2XS_UI_PUBLIC_ACCESS=false
|
||||||
HY2XS_UI_PORT=8080
|
HY2XS_UI_PORT=8080
|
||||||
HY2XS_ADMIN_USER=hy2xsadmin
|
HY2XS_ADMIN_USER=hy2xsadmin
|
||||||
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
|
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
|
||||||
@@ -30,6 +32,7 @@ HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
|
|||||||
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
|
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
|
||||||
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
|
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
|
||||||
HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml
|
HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml
|
||||||
|
HY2XS_ALLOW_LATEST_HYSTERIA=false
|
||||||
HY2XS_HYSTERIA_VERSION=v2.6.0
|
HY2XS_HYSTERIA_VERSION=v2.6.0
|
||||||
HY2XS_INSTALL_DIR=/opt/hy2xs-admin
|
HY2XS_INSTALL_DIR=/opt/hy2xs-admin
|
||||||
HY2XS_DATA_DIR=/var/lib/hy2xs-admin
|
HY2XS_DATA_DIR=/var/lib/hy2xs-admin
|
||||||
|
|||||||
Regular → Executable
@@ -16,10 +16,14 @@ Restart=on-failure
|
|||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
UMask=0077
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
ReadOnlyPaths=/etc/hysteria/config.yaml
|
||||||
ReadWritePaths={{DATA_DIR}} {{LOG_DIR}}
|
ReadWritePaths={{DATA_DIR}} {{LOG_DIR}}
|
||||||
RestrictAddressFamilies=AF_INET AF_UNIX
|
RestrictAddressFamilies=AF_INET AF_UNIX
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
LockPersonality=true
|
||||||
CapabilityBoundingSet=
|
CapabilityBoundingSet=
|
||||||
AmbientCapabilities=
|
AmbientCapabilities=
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/config.yaml
|
|||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
NoNewPrivileges=true
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом.
|
# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом.
|
||||||
|
|
||||||
DEPLOY_TARGET_OS=debian12
|
DEPLOY_TARGET_OS=debian12
|
||||||
DEPLOY_TIMESTAMP={{INSTALL_DATE}}
|
DEPLOY_TIMESTAMP={{LAST_APPLY_DATE}}
|
||||||
PACKAGE_NAME=hy2xs-install-package
|
PACKAGE_NAME=hy2xs-install-package
|
||||||
PACKAGE_BUILD_ID={{PACKAGE_BUILD_ID}}
|
PACKAGE_BUILD_ID={{PACKAGE_BUILD_ID}}
|
||||||
PACKAGE_VERSION={{PACKAGE_VERSION}}
|
PACKAGE_VERSION={{PACKAGE_VERSION}}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ table ip hy2xs {
|
|||||||
tcp dport {{SSH_PORT}} accept
|
tcp dport {{SSH_PORT}} accept
|
||||||
{{ACME_RULE}}
|
{{ACME_RULE}}
|
||||||
udp dport {{HYSTERIA_PORT}} accept
|
udp dport {{HYSTERIA_PORT}} accept
|
||||||
tcp dport {{UI_PORT}} ip saddr {{UI_BIND_HOST}} accept
|
|
||||||
icmp type echo-request accept
|
icmp type echo-request accept
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
@@ -11,6 +11,11 @@ prepare_stage() {
|
|||||||
rm -rf "$STAGE_DIR"
|
rm -rf "$STAGE_DIR"
|
||||||
mkdir -p "$STAGE_DIR" "dist"
|
mkdir -p "$STAGE_DIR" "dist"
|
||||||
copy_dir_contents "package" "$STAGE_DIR"
|
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"
|
rm -rf "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui" "$STAGE_DIR/metadata"
|
||||||
mkdir -p "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui/hy2xs-admin" "$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"
|
local archive="dist/hy2xs-install-${version}.tar.gz"
|
||||||
|
|
||||||
rm -f "$archive"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,12 @@ verify_archive() {
|
|||||||
env_content="$(tar -xOzf "$archive" hy2xs-install/config/hy2xs.env)"
|
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-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"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user