fix: harden runtime policy and remove legacy hysteria mutation paths
This commit is contained in:
@@ -4,12 +4,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"hy2xs-admin/dao"
|
"hy2xs-admin/dao"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitHysteria2() error {
|
func InitHysteria2() error {
|
||||||
@@ -28,97 +26,21 @@ func InitHysteria2() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setHysteria2ConfigYAML() error {
|
|
||||||
serverConfig, err := GetHysteria2Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if serverConfig.Listen == nil || *serverConfig.Listen == "" {
|
|
||||||
return errors.New("hysteria2 config is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
authHttpUrl, err := GetAuthHttpUrl()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverConfig.Auth == nil || serverConfig.Auth.HTTP == nil || serverConfig.Auth.HTTP.URL == nil {
|
|
||||||
if err := UpdateHysteria2Config(serverConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serverConfig, err = GetHysteria2Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update auth http url
|
|
||||||
if *serverConfig.Auth.HTTP.URL != authHttpUrl {
|
|
||||||
serverConfig.Auth.HTTP.URL = &authHttpUrl
|
|
||||||
if err := UpdateHysteria2Config(serverConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hysteria2Config, err := yaml.Marshal(&serverConfig)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("marshal hysteria2 config err: %v", err)
|
|
||||||
return errors.New("marshal hysteria2 config err")
|
|
||||||
}
|
|
||||||
tmpPath := fmt.Sprintf("%s.tmp", constant.Hysteria2ConfigPath)
|
|
||||||
file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("create hysteria2 server config file err: %v", err)
|
|
||||||
return errors.New("create hysteria2 server config file err")
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
_, err = file.WriteString(string(hysteria2Config))
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("write hysteria2 config.json file err: %v", err)
|
|
||||||
return errors.New("hysteria2 config.json file write err")
|
|
||||||
}
|
|
||||||
if syncErr := file.Sync(); syncErr != nil {
|
|
||||||
return syncErr
|
|
||||||
}
|
|
||||||
if closeErr := file.Close(); closeErr != nil {
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
if renameErr := os.Rename(tmpPath, constant.Hysteria2ConfigPath); renameErr != nil {
|
|
||||||
return renameErr
|
|
||||||
}
|
|
||||||
if chmodErr := os.Chmod(constant.Hysteria2ConfigPath, 0600); chmodErr != nil {
|
|
||||||
return chmodErr
|
|
||||||
}
|
|
||||||
if chownErr := os.Chown(constant.Hysteria2ConfigPath, 0, 0); chownErr != nil {
|
|
||||||
return chownErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hysteria2IsRunning() bool {
|
func Hysteria2IsRunning() bool {
|
||||||
_, err := util.Exec("systemctl is-active --quiet hysteria-server")
|
_, err := util.Exec("systemctl is-active --quiet hysteria-server")
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHysteria2() error {
|
func StartHysteria2() error {
|
||||||
if err := setHysteria2ConfigYAML(); err != nil {
|
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
|
||||||
return err
|
|
||||||
}
|
|
||||||
return util.Systemctl("restart", "hysteria-server")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopHysteria2() error {
|
func StopHysteria2() error {
|
||||||
return util.Systemctl("stop", "hysteria-server")
|
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RestartHysteria2() error {
|
func RestartHysteria2() error {
|
||||||
if err := StopHysteria2(); err != nil {
|
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := StartHysteria2(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReleaseHysteria2() error {
|
func ReleaseHysteria2() error {
|
||||||
|
|||||||
@@ -61,14 +61,15 @@ Hysteria2 — основной транспортный компонент се
|
|||||||
Требования:
|
Требования:
|
||||||
- production default: `acme`
|
- production default: `acme`
|
||||||
- поддерживаемые режимы: `acme | file | self_signed_dev`
|
- поддерживаемые режимы: `acme | file | self_signed_dev`
|
||||||
- `self_signed_dev` только для dev/lab
|
- `self_signed_dev` только для dev/lab и только при явном `HY2XS_ALLOW_SELF_SIGNED_DEV=true`
|
||||||
- корректный `server_name` / SNI на клиентах
|
- корректный `server_name` / SNI на клиентах
|
||||||
- одна понятная TLS policy
|
- одна понятная TLS policy
|
||||||
- без смешивания нескольких несовместимых схем по умолчанию
|
- без смешивания нескольких несовместимых схем по умолчанию
|
||||||
|
|
||||||
Инварианты:
|
Инварианты:
|
||||||
- `acme` -> только `acme` block в конфиге;
|
- `acme` -> только `acme` block в конфиге;
|
||||||
- `acme` block обязан содержать `type: http|tls|dns` из runtime env (`HY2XS_ACME_TYPE`);
|
- `acme` block обязан содержать `type: http|tls` из runtime env (`HY2XS_ACME_TYPE`);
|
||||||
|
- `HY2XS_ACME_TYPE=dns` в production-профиле запрещён до отдельной реализации;
|
||||||
- `file` -> только `tls.cert`/`tls.key` block;
|
- `file` -> только `tls.cert`/`tls.key` block;
|
||||||
- `self_signed_dev` -> только dev сценарии.
|
- `self_signed_dev` -> только dev сценарии.
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
- `HY2XS_FIREWALL_STAGED_APPLY`
|
- `HY2XS_FIREWALL_STAGED_APPLY`
|
||||||
- `HY2XS_ADMIN_USER`
|
- `HY2XS_ADMIN_USER`
|
||||||
- `HY2XS_FORCE_PASSWORD_CHANGE`
|
- `HY2XS_FORCE_PASSWORD_CHANGE`
|
||||||
|
- `HY2XS_ALLOW_SELF_SIGNED_DEV`
|
||||||
|
|
||||||
### Hysteria
|
### Hysteria
|
||||||
- `HY2_SOURCE=official-upstream`
|
- `HY2_SOURCE=official-upstream`
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
Важно:
|
Важно:
|
||||||
- `HY2XS_ADMIN_INITIAL_PASSWORD` используется только для первичного bootstrap seed;
|
- `HY2XS_ADMIN_INITIAL_PASSWORD` используется только для первичного bootstrap seed;
|
||||||
- `HY2XS_ADMIN_CON_PASS` — отдельная runtime-сущность для Hysteria auth/smoke;
|
- `HY2XS_ADMIN_CON_PASS` — отдельная runtime-сущность для Hysteria auth/smoke;
|
||||||
|
- bootstrap secret хранится в явном формате `KEY=VALUE` (`ADMIN_USER`, `ADMIN_INITIAL_PASSWORD`, `ADMIN_CON_PASS`), права `0600`;
|
||||||
- после первичного seed перезапуски `hy2xs-admin` не должны переопределять пароль admin и `con_pass`.
|
- после первичного seed перезапуски `hy2xs-admin` не должны переопределять пароль admin и `con_pass`.
|
||||||
|
|
||||||
## Что нельзя делать
|
## Что нельзя делать
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
domain: env.HY2XS_DOMAIN || "",
|
domain: env.HY2XS_DOMAIN || "",
|
||||||
publicHost: normalizePublicHost(env.HY2XS_PUBLIC_HOST || env.HY2XS_DOMAIN || ""),
|
publicHost: normalizePublicHost(env.HY2XS_PUBLIC_HOST || env.HY2XS_DOMAIN || ""),
|
||||||
publicPort: parsePort("HY2XS_PUBLIC_PORT", env.HY2XS_PUBLIC_PORT, hysteriaPort),
|
publicPort: parsePort("HY2XS_PUBLIC_PORT", env.HY2XS_PUBLIC_PORT, hysteriaPort),
|
||||||
ipv6Enabled: false,
|
ipv6Enabled: parseBool("HY2XS_IPV6_ENABLED", env.HY2XS_IPV6_ENABLED, false),
|
||||||
sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22),
|
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),
|
||||||
@@ -121,6 +121,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
|
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
|
||||||
adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)),
|
adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)),
|
||||||
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
|
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
|
||||||
|
allowSelfSignedDev: parseBool("HY2XS_ALLOW_SELF_SIGNED_DEV", env.HY2XS_ALLOW_SELF_SIGNED_DEV, false),
|
||||||
tlsMode,
|
tlsMode,
|
||||||
acmeType,
|
acmeType,
|
||||||
acmeEmail: env.HY2XS_ACME_EMAIL || "",
|
acmeEmail: env.HY2XS_ACME_EMAIL || "",
|
||||||
@@ -178,11 +179,17 @@ export function validateRuntimeConfig(config: RuntimeConfig): void {
|
|||||||
if (!config.acmeEmail) {
|
if (!config.acmeEmail) {
|
||||||
throw new Error("HY2XS_ACME_EMAIL is required for TLS mode acme");
|
throw new Error("HY2XS_ACME_EMAIL is required for TLS mode acme");
|
||||||
}
|
}
|
||||||
|
if (config.acmeType === "dns") {
|
||||||
|
throw new Error("HY2XS_ACME_TYPE=dns is not supported in production profile yet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.tlsMode === "file") {
|
if (config.tlsMode === "file") {
|
||||||
requireValue("HY2XS_TLS_CERT_PATH", config.tlsCertPath);
|
requireValue("HY2XS_TLS_CERT_PATH", config.tlsCertPath);
|
||||||
requireValue("HY2XS_TLS_KEY_PATH", config.tlsKeyPath);
|
requireValue("HY2XS_TLS_KEY_PATH", config.tlsKeyPath);
|
||||||
}
|
}
|
||||||
|
if (config.tlsMode === "self_signed_dev" && !config.allowSelfSignedDev) {
|
||||||
|
throw new Error("HY2XS_TLS_MODE=self_signed_dev requires HY2XS_ALLOW_SELF_SIGNED_DEV=true");
|
||||||
|
}
|
||||||
|
|
||||||
if (config.publicHost === "0.0.0.0") {
|
if (config.publicHost === "0.0.0.0") {
|
||||||
throw new Error("HY2XS_PUBLIC_HOST must be a routable domain or IPv4, not 0.0.0.0");
|
throw new Error("HY2XS_PUBLIC_HOST must be a routable domain or IPv4, not 0.0.0.0");
|
||||||
@@ -205,6 +212,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
|
|||||||
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
|
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
|
||||||
`HY2XS_ADMIN_CON_PASS=${config.adminConPass}`,
|
`HY2XS_ADMIN_CON_PASS=${config.adminConPass}`,
|
||||||
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
|
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
|
||||||
|
`HY2XS_ALLOW_SELF_SIGNED_DEV=${config.allowSelfSignedDev}`,
|
||||||
`HY2XS_TLS_MODE=${config.tlsMode}`,
|
`HY2XS_TLS_MODE=${config.tlsMode}`,
|
||||||
`HY2XS_ACME_TYPE=${config.acmeType}`,
|
`HY2XS_ACME_TYPE=${config.acmeType}`,
|
||||||
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
|
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export async function generateConfig(context: InstallContext): Promise<void> {
|
|||||||
UI_PORT: context.config.uiPort,
|
UI_PORT: context.config.uiPort,
|
||||||
BANDWIDTH_UP: context.config.hysteriaBandwidthUp,
|
BANDWIDTH_UP: context.config.hysteriaBandwidthUp,
|
||||||
BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown,
|
BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown,
|
||||||
|
IGNORE_CLIENT_BANDWIDTH: context.config.hysteriaIgnoreClientBandwidth ? "true" : "false",
|
||||||
TLS_ACME_BLOCK: tlsAcmeBlock,
|
TLS_ACME_BLOCK: tlsAcmeBlock,
|
||||||
TLS_FILE_BLOCK: tlsFileBlock,
|
TLS_FILE_BLOCK: tlsFileBlock,
|
||||||
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
|
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
|
||||||
|
|||||||
@@ -30,5 +30,13 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
|
|||||||
});
|
});
|
||||||
|
|
||||||
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
|
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
|
||||||
await writeText(context.config.bootstrapAdminSecretPath, `${context.config.adminUser}:${context.config.adminConPass}\n`, 0o600);
|
await writeText(
|
||||||
|
context.config.bootstrapAdminSecretPath,
|
||||||
|
[
|
||||||
|
`ADMIN_USER=${context.config.adminUser}`,
|
||||||
|
`ADMIN_INITIAL_PASSWORD=${context.config.adminInitialPassword}`,
|
||||||
|
`ADMIN_CON_PASS=${context.config.adminConPass}`
|
||||||
|
].join("\n") + "\n",
|
||||||
|
0o600
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function smoke(context: InstallContext): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await runVisible`systemctl start hysteria-server hy2xs-admin`;
|
await runVisible`systemctl restart hysteria-server hy2xs-admin`;
|
||||||
await runVisible`systemctl is-active --quiet hysteria-server`;
|
await runVisible`systemctl is-active --quiet hysteria-server`;
|
||||||
await runVisible`systemctl is-active --quiet hy2xs-admin`;
|
await runVisible`systemctl is-active --quiet hy2xs-admin`;
|
||||||
await runVisible`/usr/local/bin/hysteria version`;
|
await runVisible`/usr/local/bin/hysteria version`;
|
||||||
@@ -16,7 +16,9 @@ export async function smoke(context: InstallContext): Promise<void> {
|
|||||||
await runVisible`test -s /etc/hy2xs/hy2xs.env`;
|
await runVisible`test -s /etc/hy2xs/hy2xs.env`;
|
||||||
await runVisible`test -s /etc/hysteria/post-install.env`;
|
await runVisible`test -s /etc/hysteria/post-install.env`;
|
||||||
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
|
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
|
||||||
await runVisible`grep -q '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath}`;
|
await runVisible`grep -q '^ADMIN_USER=' ${context.config.bootstrapAdminSecretPath}`;
|
||||||
|
await runVisible`grep -q '^ADMIN_INITIAL_PASSWORD=' ${context.config.bootstrapAdminSecretPath}`;
|
||||||
|
await runVisible`grep -q '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath}`;
|
||||||
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
|
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
|
||||||
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 '%a' /etc/hysteria/post-install.env)" = '600'`;
|
await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`;
|
||||||
@@ -32,7 +34,7 @@ export async function smoke(context: InstallContext): Promise<void> {
|
|||||||
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
|
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminConPass = (await runSecret`grep '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d: -f2-`).trim();
|
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
|
||||||
if (!adminConPass) {
|
if (!adminConPass) {
|
||||||
throw new Error("admin connection password is empty in bootstrap secret file");
|
throw new Error("admin connection password is empty in bootstrap secret file");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export type RuntimeConfig = {
|
|||||||
domain: string;
|
domain: string;
|
||||||
publicHost: string;
|
publicHost: string;
|
||||||
publicPort: number;
|
publicPort: number;
|
||||||
ipv6Enabled: false;
|
ipv6Enabled: boolean;
|
||||||
sshPort: number;
|
sshPort: number;
|
||||||
firewallEnabled: boolean;
|
firewallEnabled: boolean;
|
||||||
firewallStagedApply: boolean;
|
firewallStagedApply: boolean;
|
||||||
@@ -34,6 +34,7 @@ export type RuntimeConfig = {
|
|||||||
adminInitialPassword: string;
|
adminInitialPassword: string;
|
||||||
adminConPass: string;
|
adminConPass: string;
|
||||||
forcePasswordChange: boolean;
|
forcePasswordChange: boolean;
|
||||||
|
allowSelfSignedDev: boolean;
|
||||||
tlsMode: TlsMode;
|
tlsMode: TlsMode;
|
||||||
acmeType: "http" | "tls" | "dns";
|
acmeType: "http" | "tls" | "dns";
|
||||||
acmeEmail: string;
|
acmeEmail: string;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ 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=true
|
HY2XS_FORCE_PASSWORD_CHANGE=false
|
||||||
|
HY2XS_ALLOW_SELF_SIGNED_DEV=false
|
||||||
HY2XS_TLS_MODE=acme
|
HY2XS_TLS_MODE=acme
|
||||||
HY2XS_ACME_TYPE=http
|
HY2XS_ACME_TYPE=http
|
||||||
HY2XS_ACME_EMAIL=replace-with-your-email@example.com
|
HY2XS_ACME_EMAIL=replace-with-your-email@example.com
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ bandwidth:
|
|||||||
up: "{{BANDWIDTH_UP}}"
|
up: "{{BANDWIDTH_UP}}"
|
||||||
down: "{{BANDWIDTH_DOWN}}"
|
down: "{{BANDWIDTH_DOWN}}"
|
||||||
|
|
||||||
|
ignoreClientBandwidth: {{IGNORE_CLIENT_BANDWIDTH}}
|
||||||
|
|
||||||
trafficStats:
|
trafficStats:
|
||||||
listen: {{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}}
|
listen: {{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}}
|
||||||
secret: "{{HYSTERIA_API_SECRET}}"
|
secret: "{{HYSTERIA_API_SECRET}}"
|
||||||
|
|||||||
Reference in New Issue
Block a user