5eeb37389b
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.
121 lines
3.0 KiB
Go
121 lines
3.0 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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)
|
|
|
|
v.SetDefault("database.driver", "sqlite3")
|
|
v.SetDefault("database.sqlite.path", "data/mygo.db")
|
|
v.SetDefault("database.postgres.host", "localhost")
|
|
v.SetDefault("database.postgres.port", 5432)
|
|
v.SetDefault("database.postgres.user", "mygo")
|
|
v.SetDefault("database.postgres.password", "")
|
|
v.SetDefault("database.postgres.dbname", "mygo")
|
|
v.SetDefault("database.postgres.sslmode", "disable")
|
|
|
|
v.SetDefault("storage.driver", "local")
|
|
v.SetDefault("storage.local.path", "data/files")
|
|
|
|
v.SetDefault("jwt.secret", DefaultJWTSecret)
|
|
v.SetDefault("jwt.access_ttl", "15m")
|
|
v.SetDefault("jwt.refresh_ttl", "168h")
|
|
|
|
v.SetDefault("log.level", "info")
|
|
v.SetDefault("log.file_path", "")
|
|
v.SetDefault("log.file_level", "debug")
|
|
}
|
|
|
|
func New() *viper.Viper {
|
|
v := viper.New()
|
|
v.SetEnvPrefix("MYGO")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.AutomaticEnv()
|
|
defaults(v)
|
|
return v
|
|
}
|
|
|
|
func Load(v *viper.Viper, cfgFile string) (*Config, LoadInfo, error) {
|
|
if cfgFile != "" {
|
|
v.SetConfigFile(cfgFile)
|
|
} else {
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath(".")
|
|
}
|
|
|
|
if err := v.ReadInConfig(); err != nil {
|
|
var notFound viper.ConfigFileNotFoundError
|
|
if !errors.As(err, ¬Found) {
|
|
return nil, LoadInfo{}, fmt.Errorf("read config: %w", err)
|
|
}
|
|
}
|
|
|
|
var cfg Config
|
|
if err := v.Unmarshal(&cfg); err != nil {
|
|
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, LoadInfo{}, fmt.Errorf("validate config: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|