- resolveSecrets 统一所有部署形态的零配置体验:显式 env 优先 → DATA_DIR/.secrets.env → 自动生成随机密钥持久化(0600,重启不变);无法持久化才拒绝启动(单测覆盖生成/复用/显式优先) - install.md 重写为三方式各一条命令(K8s install.sh / docker compose up -d / release 包), 密钥与数据说明收敛到统一「说明」一节;README Docker 段同步精简
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
var (
|
|
instance *Config
|
|
once sync.Once
|
|
)
|
|
|
|
type Config struct {
|
|
ServerPort string
|
|
DatabasePath string
|
|
JWTSecret string
|
|
EncryptionKey string // 独立加密密钥,必须与JWT_SECRET分离
|
|
DataDir string
|
|
KubeDir string
|
|
LogLevel string // 新增:日志级别 debug/info/warn/error
|
|
HostKeyDir string // 新增:SSH主机密钥存储目录
|
|
}
|
|
|
|
// resolveSecrets 密钥解析(所有部署形态统一的零配置体验):
|
|
// 显式环境变量优先;缺失时读取 DATA_DIR/.secrets.env;两者皆无则生成随机密钥
|
|
// 并持久化(0600),重启/升级保持不变。无法持久化时拒绝启动,绝不使用易失密钥。
|
|
func resolveSecrets(dataDir string) (jwt, enc string) {
|
|
jwt = os.Getenv("JWT_SECRET")
|
|
enc = os.Getenv("ENCRYPTION_KEY")
|
|
if jwt != "" && enc != "" {
|
|
return jwt, enc
|
|
}
|
|
secretsFile := filepath.Join(dataDir, ".secrets.env")
|
|
if data, err := os.ReadFile(secretsFile); err == nil {
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if v, ok := strings.CutPrefix(line, "JWT_SECRET="); ok && jwt == "" && v != "" {
|
|
jwt = v
|
|
}
|
|
if v, ok := strings.CutPrefix(line, "ENCRYPTION_KEY="); ok && enc == "" && v != "" {
|
|
enc = v
|
|
}
|
|
}
|
|
}
|
|
if jwt != "" && enc != "" {
|
|
return jwt, enc
|
|
}
|
|
gen := func() string {
|
|
buf := make([]byte, 32)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
log.Fatal("[SECURITY] 生成随机密钥失败: ", err)
|
|
}
|
|
return hex.EncodeToString(buf)
|
|
}
|
|
if jwt == "" {
|
|
jwt = gen()
|
|
log.Printf("[SECURITY] JWT_SECRET 未设置,已生成随机密钥并持久化到 %s", secretsFile)
|
|
}
|
|
if enc == "" {
|
|
enc = gen()
|
|
log.Printf("[SECURITY] ENCRYPTION_KEY 未设置,已生成随机密钥并持久化到 %s", secretsFile)
|
|
}
|
|
if err := os.MkdirAll(dataDir, 0o700); err != nil {
|
|
log.Fatal("[SECURITY] 无法创建数据目录以持久化密钥: ", err)
|
|
}
|
|
if err := os.WriteFile(secretsFile, []byte(fmt.Sprintf(
|
|
"# LokiStack auto-generated. Delete to regenerate.\nJWT_SECRET=%s\nENCRYPTION_KEY=%s\n", jwt, enc,
|
|
)), 0o600); err != nil {
|
|
log.Fatal("[SECURITY] 密钥持久化失败(请显式设置 JWT_SECRET / ENCRYPTION_KEY): ", err)
|
|
}
|
|
return jwt, enc
|
|
}
|
|
|
|
func GetConfig() *Config {
|
|
once.Do(func() {
|
|
dataDir := getEnv("DATA_DIR", "./data")
|
|
|
|
jwtSecret, encKey := resolveSecrets(dataDir)
|
|
|
|
// ENCRYPTION_KEY 长度校验:至少32字节(256位)
|
|
if len(encKey) < 32 {
|
|
log.Fatal(fmt.Sprintf("[SECURITY] ENCRYPTION_KEY must be at least 32 bytes, got %d bytes", len(encKey)))
|
|
}
|
|
|
|
instance = &Config{
|
|
ServerPort: getEnv("SERVER_PORT", "8080"),
|
|
DatabasePath: databasePath(dataDir),
|
|
JWTSecret: jwtSecret,
|
|
EncryptionKey: encKey,
|
|
DataDir: dataDir,
|
|
KubeDir: filepath.Join(dataDir, "kubeconfigs"),
|
|
LogLevel: getEnv("LOG_LEVEL", "info"),
|
|
HostKeyDir: filepath.Join(dataDir, "known_hosts"),
|
|
}
|
|
|
|
_ = os.MkdirAll(instance.KubeDir, 0700)
|
|
_ = os.MkdirAll(instance.HostKeyDir, 0700)
|
|
})
|
|
return instance
|
|
}
|
|
|
|
func getEnv(key, defaultVal string) string {
|
|
if val := os.Getenv(key); val != "" {
|
|
return val
|
|
}
|
|
return defaultVal
|
|
}
|
|
|
|
// databasePath 新名优先;历史命名的数据文件存在且新文件不存在时沿用旧文件
|
|
// (数据平滑迁移,不搬移文件、零丢失风险)
|
|
func databasePath(dataDir string) string {
|
|
newPath := filepath.Join(dataDir, "lokistack.json")
|
|
if _, err := os.Stat(newPath); err == nil {
|
|
return newPath
|
|
}
|
|
for _, legacy := range []string{"opsxloki.json", "lokideploy.json"} {
|
|
legacyPath := filepath.Join(dataDir, legacy)
|
|
if _, err := os.Stat(legacyPath); err == nil {
|
|
return legacyPath
|
|
}
|
|
}
|
|
return newPath
|
|
}
|