fix(security): 分离 JWT 与数据加密密钥并新增启动安全体检
.env 中 JWT_SECRET 仍逐字节等于 .env.example 的示例值,且 DATA_ENCRYPTION_KEY 未设置、按 config.go 的回退逻辑实际等于 JWT_SECRET。任何见过仓库的人都能伪造 管理员 JWT,并解密库中全部 API Client / 回调密钥密文。 - JWT_SECRET 与 DATA_ENCRYPTION_KEY 改为各自独立的强随机值 - 新增 Config.SecurityWarnings():示例密钥 / 未分离 / release 下开调试日志均告警 - 新增 cmd/reencrypt_secrets:默认只预演,-apply 才写库;幂等,坏数据不中断整轮 - 新增 docs/密钥轮换说明.md;.gitignore 忽略 .env.bak* - release 模式下关闭 OPEN_API_DEBUG
This commit is contained in:
@@ -104,6 +104,34 @@ func Load() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// 仓库 .env.example 中的占位密钥。这些值对所有人公开,一旦被带到真实部署,
|
||||
// JWT 可被伪造、落库密文可被解密,因此启动时必须显式告警。
|
||||
const (
|
||||
exampleJWTSecret = "your_very_secure_jwt_secret_here_change_me_in_production"
|
||||
exampleDataKey = "your_32_plus_char_data_encryption_key_change_me"
|
||||
)
|
||||
|
||||
// SecurityWarnings 返回启动时的密钥配置风险清单;返回空切片表示配置正常。
|
||||
// 刻意只告警不阻断:升级过程直接 fatal 会让存量部署起不来,风险由运维可见性兜住。
|
||||
func (c *Config) SecurityWarnings() []string {
|
||||
var warnings []string
|
||||
if c.JWTSecret == exampleJWTSecret {
|
||||
warnings = append(warnings, "JWT_SECRET 仍是 .env.example 里的示例值,任何见过仓库的人都能伪造管理员 Token")
|
||||
}
|
||||
if os.Getenv("DATA_ENCRYPTION_KEY") == "" {
|
||||
warnings = append(warnings, "DATA_ENCRYPTION_KEY 未显式设置,已回退到 JWT_SECRET;请设置独立密钥,更换后需用 cmd/reencrypt_secrets 重加密历史密文")
|
||||
} else if c.DataEncryptionKey == exampleDataKey {
|
||||
warnings = append(warnings, "DATA_ENCRYPTION_KEY 仍是 .env.example 里的示例值,落库的 API/回调密钥可被解密")
|
||||
}
|
||||
if c.DataEncryptionKey == c.JWTSecret {
|
||||
warnings = append(warnings, "DATA_ENCRYPTION_KEY 与 JWT_SECRET 相同,建议分离以便独立轮换")
|
||||
}
|
||||
if c.Mode == "release" && c.OpenAPIDebug {
|
||||
warnings = append(warnings, "GIN_MODE=release 下开启了 OPEN_API_DEBUG,开放接口报文详情会持续写盘")
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// getEnvList 解析逗号分隔的字符串列表(如 CORS 来源);空串返回默认值。
|
||||
func getEnvList(key string, def []string) []string {
|
||||
v := os.Getenv(key)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// containsWarning 判断告警列表里是否存在包含指定片段的告警。
|
||||
func containsWarning(warnings []string, fragment string) bool {
|
||||
for _, w := range warnings {
|
||||
if strings.Contains(w, fragment) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestSecurityWarnings(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
dataKeyEnv string
|
||||
cfg Config
|
||||
wantWarning string // 期望命中的告警片段;为空表示期望无任何告警
|
||||
}{
|
||||
{
|
||||
name: "示例 JWT_SECRET 必须告警",
|
||||
dataKeyEnv: "a-real-data-key",
|
||||
cfg: Config{JWTSecret: exampleJWTSecret, DataEncryptionKey: "a-real-data-key", Mode: "debug"},
|
||||
wantWarning: "JWT_SECRET",
|
||||
},
|
||||
{
|
||||
name: "未显式设置 DATA_ENCRYPTION_KEY 时提示回退 JWT_SECRET",
|
||||
dataKeyEnv: "",
|
||||
cfg: Config{JWTSecret: "a-real-jwt-secret", DataEncryptionKey: "a-real-jwt-secret", Mode: "debug"},
|
||||
wantWarning: "DATA_ENCRYPTION_KEY 未显式设置",
|
||||
},
|
||||
{
|
||||
name: "示例 DATA_ENCRYPTION_KEY 必须告警",
|
||||
dataKeyEnv: exampleDataKey,
|
||||
cfg: Config{JWTSecret: "a-real-jwt-secret", DataEncryptionKey: exampleDataKey, Mode: "debug"},
|
||||
wantWarning: "DATA_ENCRYPTION_KEY 仍是",
|
||||
},
|
||||
{
|
||||
name: "release 模式开启 OPEN_API_DEBUG 必须告警",
|
||||
dataKeyEnv: "a-real-data-key",
|
||||
cfg: Config{JWTSecret: "a-real-jwt-secret", DataEncryptionKey: "a-real-data-key", Mode: "release", OpenAPIDebug: true},
|
||||
wantWarning: "OPEN_API_DEBUG",
|
||||
},
|
||||
{
|
||||
name: "健康配置不应产生任何告警",
|
||||
dataKeyEnv: "a-real-data-key",
|
||||
cfg: Config{
|
||||
JWTSecret: "a-real-jwt-secret",
|
||||
DataEncryptionKey: "a-real-data-key",
|
||||
Mode: "release",
|
||||
OpenAPIDebug: false,
|
||||
},
|
||||
wantWarning: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("DATA_ENCRYPTION_KEY", tc.dataKeyEnv)
|
||||
warnings := tc.cfg.SecurityWarnings()
|
||||
|
||||
if tc.wantWarning == "" {
|
||||
if len(warnings) != 0 {
|
||||
t.Fatalf("期望无告警,实际得到 %v", warnings)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !containsWarning(warnings, tc.wantWarning) {
|
||||
t.Fatalf("期望告警包含 %q,实际得到 %v", tc.wantWarning, warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user