fix(backend): secure production startup and migrations
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -43,6 +44,17 @@ func LoadFromDir(configDir string) (Config, error) {
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
selectedEnvironment, err := canonicalEnvironment(mode)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
configuredEnvironment, err := canonicalEnvironment(cfg.Env)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if selectedEnvironment != configuredEnvironment {
|
||||
return Config{}, fmt.Errorf("SENLIN_APP_MODE %s does not match config env %s", mode, cfg.Env)
|
||||
}
|
||||
if cfg.MaxUploadBytes <= 0 {
|
||||
cfg.MaxUploadBytes = 32 << 20
|
||||
}
|
||||
@@ -59,28 +71,57 @@ func LoadFromDir(configDir string) (Config, error) {
|
||||
if !hasAllowedOrigin {
|
||||
return Config{}, fmt.Errorf("allowed_origins must include at least one origin")
|
||||
}
|
||||
if err := validateSecret(cfg.Env, "auth_secret", cfg.AuthSecret); err != nil {
|
||||
production := selectedEnvironment == "production"
|
||||
if err := validateSecret(production, "auth_secret", cfg.AuthSecret); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateSecret(cfg.Env, "ai_key_encryption_secret", cfg.AIKeyEncryptionSecret); err != nil {
|
||||
if err := validateSecret(production, "ai_key_encryption_secret", cfg.AIKeyEncryptionSecret); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func validateSecret(environment, field, value string) error {
|
||||
func canonicalEnvironment(value string) (string, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "dev", "development":
|
||||
return "development", nil
|
||||
case "prod", "production":
|
||||
return "production", nil
|
||||
case "test":
|
||||
return "test", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported environment %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func validateSecret(production bool, field, value string) error {
|
||||
secret := strings.TrimSpace(value)
|
||||
if secret == "" {
|
||||
return fmt.Errorf("%s must not be empty", field)
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(environment), "production") || strings.EqualFold(strings.TrimSpace(environment), "prod") {
|
||||
if len(secret) < 32 || isCommonSecret(secret) {
|
||||
return fmt.Errorf("%s must be at least 32 characters and must not use a development sentinel in production", field)
|
||||
if production {
|
||||
if len([]byte(secret)) < 32 || estimatedEntropyBits(secret) < 128 || isCommonSecret(secret) {
|
||||
return fmt.Errorf("%s must contain at least 32 bytes and an estimated 128 bits of entropy, without development sentinels, in production", field)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func estimatedEntropyBits(value string) float64 {
|
||||
data := []byte(value)
|
||||
counts := make(map[byte]int, len(data))
|
||||
for _, item := range data {
|
||||
counts[item]++
|
||||
}
|
||||
length := float64(len(data))
|
||||
entropyPerByte := 0.0
|
||||
for _, count := range counts {
|
||||
probability := float64(count) / length
|
||||
entropyPerByte -= probability * math.Log2(probability)
|
||||
}
|
||||
return entropyPerByte * length
|
||||
}
|
||||
|
||||
func isCommonSecret(value string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
for _, marker := range []string{"change-me", "changeme", "development", "dev-secret", "local-secret", "test-secret", "placeholder"} {
|
||||
|
||||
Reference in New Issue
Block a user