fix(config): harden default JWT secret

Replace known development JWT secret placeholders with an ephemeral runtime secret during config loading.

Return LoadInfo so startup can warn when token persistence depends on a stable configured secret.

Update config docs and tests for default, legacy placeholder, custom, env, and empty secret behavior.
This commit is contained in:
2026-07-05 13:25:44 +08:00
parent 7b6587a951
commit 5eeb37389b
6 changed files with 131 additions and 16 deletions
+55 -6
View File
@@ -1,6 +1,8 @@
package config
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"strings"
@@ -8,6 +10,17 @@ import (
"github.com/spf13/viper"
)
// DefaultJWTSecret is a development-only placeholder. Load replaces it with
// an ephemeral secret before returning a usable Config.
const DefaultJWTSecret = "dev-secret-do-not-use-in-production"
// LoadInfo describes runtime decisions made while loading configuration.
type LoadInfo struct {
// EphemeralJWTSecret is true when a placeholder jwt.secret was replaced
// with a random in-memory value for the current process.
EphemeralJWTSecret bool
}
func defaults(v *viper.Viper) {
v.SetDefault("server.host", "0.0.0.0")
v.SetDefault("server.port", 10086)
@@ -24,7 +37,7 @@ func defaults(v *viper.Viper) {
v.SetDefault("storage.driver", "local")
v.SetDefault("storage.local.path", "data/files")
v.SetDefault("jwt.secret", "dev-secret-do-not-use-in-production")
v.SetDefault("jwt.secret", DefaultJWTSecret)
v.SetDefault("jwt.access_ttl", "15m")
v.SetDefault("jwt.refresh_ttl", "168h")
@@ -42,7 +55,7 @@ func New() *viper.Viper {
return v
}
func Load(v *viper.Viper, cfgFile string) (*Config, error) {
func Load(v *viper.Viper, cfgFile string) (*Config, LoadInfo, error) {
if cfgFile != "" {
v.SetConfigFile(cfgFile)
} else {
@@ -54,18 +67,54 @@ func Load(v *viper.Viper, cfgFile string) (*Config, error) {
if err := v.ReadInConfig(); err != nil {
var notFound viper.ConfigFileNotFoundError
if !errors.As(err, &notFound) {
return nil, fmt.Errorf("read config: %w", err)
return nil, LoadInfo{}, fmt.Errorf("read config: %w", err)
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
return nil, LoadInfo{}, fmt.Errorf("unmarshal config: %w", err)
}
info, err := hardenRuntimeSecrets(&cfg)
if err != nil {
return nil, LoadInfo{}, err
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
return nil, LoadInfo{}, fmt.Errorf("validate config: %w", err)
}
return &cfg, nil
return &cfg, info, nil
}
func hardenRuntimeSecrets(cfg *Config) (LoadInfo, error) {
if !isWeakJWTSecret(cfg.JWT.Secret) {
return LoadInfo{}, nil
}
secret, err := generateEphemeralJWTSecret()
if err != nil {
return LoadInfo{}, fmt.Errorf("generate ephemeral jwt secret: %w", err)
}
cfg.JWT.Secret = secret
return LoadInfo{EphemeralJWTSecret: true}, nil
}
func isWeakJWTSecret(secret string) bool {
switch secret {
case DefaultJWTSecret, "change-me-in-production", "changeme-in-production":
return true
default:
return false
}
}
func generateEphemeralJWTSecret() (string, error) {
var secret [32]byte
if _, err := rand.Read(secret[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(secret[:]), nil
}