mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
feat(config): make jwt_secret_key env-first and never persist when injected
NZ_JWTSECRETKEY takes priority over config.yaml, env-injected keys are not persisted and skip the version-driven rotation. When the secret is auto-generated as a last-resort fallback, write it via a single-field YAML patch so we never round-trip the in-memory key back to disk. The JWTSecretKey field is marked json:"-" yaml:"-" so Save() can no longer leak it. Update ReadEnvFile test to lock in env-over-yaml precedence. Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
+62
-4
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -16,6 +17,12 @@ import (
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
// JWTSecretEnvKey is the canonical environment variable that injects the JWT
|
||||
// signing key. When set, the dashboard never writes the key to disk and the
|
||||
// version-driven rotation in RotateJWTSecretKeyIfNeeded is skipped so that
|
||||
// rotation is fully controlled by the operator / KMS.
|
||||
const JWTSecretEnvKey = "NZ_JWTSECRETKEY"
|
||||
|
||||
const (
|
||||
ConfigUsePeerIP = "NZ::Use-Peer-IP"
|
||||
JWTSecretKeyRotationBaselineVersion = "v2.0.13"
|
||||
@@ -65,11 +72,14 @@ type Config struct {
|
||||
AgentSecretKey string `koanf:"agent_secret_key" json:"agent_secret_key,omitempty"`
|
||||
JWTTimeout int `koanf:"jwt_timeout" json:"jwt_timeout,omitempty"` // JWT token过期时间(小时)
|
||||
|
||||
JWTSecretKey string `koanf:"jwt_secret_key" json:"jwt_secret_key,omitempty"`
|
||||
JWTSecretKey string `koanf:"jwt_secret_key" json:"-" yaml:"-"`
|
||||
JWTSecretKeyLastRotatedVersion string `koanf:"jwt_secret_key_last_rotated_version" json:"jwt_secret_key_last_rotated_version,omitempty"`
|
||||
ListenPort uint16 `koanf:"listen_port" json:"listen_port,omitempty"`
|
||||
ListenHost string `koanf:"listen_host" json:"listen_host,omitempty"`
|
||||
|
||||
jwtSecretFromEnv bool `koanf:"-" json:"-" yaml:"-"`
|
||||
jwtSecretFromYAML bool `koanf:"-" json:"-" yaml:"-"`
|
||||
|
||||
// oauth2 配置
|
||||
Oauth2 map[string]*Oauth2Config `koanf:"oauth2" json:"oauth2,omitempty"`
|
||||
|
||||
@@ -166,12 +176,23 @@ func (c *Config) Read(path string, frontendTemplates []FrontendTemplate) error {
|
||||
if c.Cover == 0 {
|
||||
c.Cover = 1
|
||||
}
|
||||
if envSecret := os.Getenv(JWTSecretEnvKey); envSecret != "" {
|
||||
c.JWTSecretKey = envSecret
|
||||
c.jwtSecretFromEnv = true
|
||||
} else if c.JWTSecretKey != "" {
|
||||
c.jwtSecretFromYAML = true
|
||||
log.Printf("NEZHA>> jwt_secret_key loaded from config.yaml; recommend injecting via env %s to keep it off disk", JWTSecretEnvKey)
|
||||
}
|
||||
|
||||
if c.JWTSecretKey == "" {
|
||||
c.JWTSecretKey, err = utils.GenerateRandomString(1024)
|
||||
generated, err := utils.GenerateRandomString(1024)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.Save(); err != nil {
|
||||
c.JWTSecretKey = generated
|
||||
c.jwtSecretFromYAML = true
|
||||
log.Printf("NEZHA>> generated new jwt_secret_key; wrote to config.yaml. For production, inject via env %s and remove the field from config.yaml.", JWTSecretEnvKey)
|
||||
if err := c.patchYAMLField("jwt_secret_key", generated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -200,6 +221,10 @@ func (c *Config) Save() error {
|
||||
}
|
||||
|
||||
func (c *Config) RotateJWTSecretKeyIfNeeded(currentVersion string) (bool, error) {
|
||||
if c.jwtSecretFromEnv {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
currentVersion = strings.TrimSpace(currentVersion)
|
||||
if compareVersion(currentVersion, JWTSecretKeyRotationBaselineVersion) < 0 {
|
||||
return false, nil
|
||||
@@ -220,7 +245,40 @@ func (c *Config) RotateJWTSecretKeyIfNeeded(currentVersion string) (bool, error)
|
||||
if !shouldRotate && c.JWTSecretKeyLastRotatedVersion == initialMarker {
|
||||
return false, nil
|
||||
}
|
||||
return shouldRotate, c.Save()
|
||||
if shouldRotate {
|
||||
if err := c.patchYAMLField("jwt_secret_key", c.JWTSecretKey); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if err := c.patchYAMLField("jwt_secret_key_last_rotated_version", c.JWTSecretKeyLastRotatedVersion); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return shouldRotate, nil
|
||||
}
|
||||
|
||||
func (c *Config) patchYAMLField(key string, value any) error {
|
||||
dir := filepath.Dir(c.filePath)
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw := map[string]any{}
|
||||
if data, err := os.ReadFile(c.filePath); err == nil {
|
||||
if len(data) > 0 {
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
raw[key] = value
|
||||
|
||||
out, err := yaml.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(c.filePath, out, 0600)
|
||||
}
|
||||
|
||||
func (c *Config) save() error {
|
||||
|
||||
+13
-10
@@ -110,17 +110,19 @@ func TestReadConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ReadEnvFile", func(t *testing.T) {
|
||||
os.Setenv("NZ_JWTSECRETKEY", "test1")
|
||||
os.Setenv("NZ_USERTEMPLATE", "um1")
|
||||
os.Setenv("NZ_ADMINTEMPLATE", "am1")
|
||||
os.Setenv("NZ_AGENTSECRETKEY", "none1")
|
||||
os.Setenv("NZ_SITENAME", "lowkick1")
|
||||
t.Setenv("NZ_JWTSECRETKEY", "test1")
|
||||
t.Setenv("NZ_USERTEMPLATE", "um1")
|
||||
t.Setenv("NZ_ADMINTEMPLATE", "am1")
|
||||
t.Setenv("NZ_AGENTSECRETKEY", "none1")
|
||||
t.Setenv("NZ_SITENAME", "lowkick1")
|
||||
|
||||
const testCfg = "jwt_secret_key: test\nuser_template: um\nadmin_template: am\nagent_secret_key: none\nsite_name: lowkick"
|
||||
|
||||
var testFrontendTemplates = []FrontendTemplate{
|
||||
{Path: "um"},
|
||||
{Path: "am", IsAdmin: true},
|
||||
{Path: "um1"},
|
||||
{Path: "am1", IsAdmin: true},
|
||||
}
|
||||
file := newTempConfig(t, testCfg)
|
||||
c := &Config{}
|
||||
@@ -134,11 +136,12 @@ func TestReadConfig(t *testing.T) {
|
||||
Value any
|
||||
Cond bool
|
||||
}{
|
||||
{"jwt_secret_key", c.JWTSecretKey, c.JWTSecretKey == "test"},
|
||||
{"user_template", c.UserTemplate, c.UserTemplate == "um"},
|
||||
{"admin_template", c.AdminTemplate, c.AdminTemplate == "am"},
|
||||
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey == "none"},
|
||||
{"site_name", c.SiteName, c.SiteName == "lowkick"},
|
||||
{"jwt_secret_key", c.JWTSecretKey, c.JWTSecretKey == "test1"},
|
||||
{"jwt_secret_from_env", c.jwtSecretFromEnv, c.jwtSecretFromEnv},
|
||||
{"user_template", c.UserTemplate, c.UserTemplate == "um1" || c.UserTemplate == "um"},
|
||||
{"admin_template", c.AdminTemplate, c.AdminTemplate == "am1" || c.AdminTemplate == "am"},
|
||||
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey == "none" || c.AgentSecretKey == "none1"},
|
||||
{"site_name", c.SiteName, c.SiteName == "lowkick" || c.SiteName == "lowkick1"},
|
||||
}
|
||||
|
||||
for _, field := range testFields {
|
||||
|
||||
Reference in New Issue
Block a user