diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index a0adb8e7..d8ac9b9d 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -109,17 +109,38 @@ func Load() (*Config, error) { if cfg.MagicCodeSecret == "" { missing = append(missing, "MAGIC_CODE_SECRET") } - if os.Getenv("INSTANCE_ENCRYPTION_KEY") == "" { + encKey := os.Getenv("INSTANCE_ENCRYPTION_KEY") + if encKey == "" { missing = append(missing, "INSTANCE_ENCRYPTION_KEY") } if len(missing) > 0 { return nil, fmt.Errorf("missing required production secrets %v (set ENV=development only for local dev)", missing) } + // A set-but-weak encryption key is as dangerous as a missing one: it + // feeds the AES key that protects instance secrets (SMTP password, OAuth + // client secrets, GitHub App private key). Reject the shipped placeholder + // and anything too short to be meaningfully random. + if isWeakSecret(encKey) { + return nil, fmt.Errorf("INSTANCE_ENCRYPTION_KEY is a placeholder or too short; set a random value of at least 16 characters for production") + } } return cfg, nil } +// isWeakSecret reports whether a production secret is unusable: a known +// .env.example placeholder, or too short to carry meaningful entropy. +func isWeakSecret(v string) bool { + if len(v) < 16 { + return true + } + switch v { + case "change-me-generate-a-random-key", "change-me", "changeme": + return true + } + return false +} + func getEnv(key, defaultVal string) string { if v := os.Getenv(key); v != "" { return v diff --git a/apps/api/internal/config/config_test.go b/apps/api/internal/config/config_test.go new file mode 100644 index 00000000..eff9eb38 --- /dev/null +++ b/apps/api/internal/config/config_test.go @@ -0,0 +1,29 @@ +package config + +import "testing" + +func TestIsWeakSecret(t *testing.T) { + weak := []string{ + "", + "short", + "change-me-generate-a-random-key", + "change-me", + "changeme", + "0123456789abcde", // 15 chars + } + for _, v := range weak { + if !isWeakSecret(v) { + t.Errorf("isWeakSecret(%q) = false; want true", v) + } + } + + strong := []string{ + "0123456789abcdef", // 16 chars + "a-perfectly-fine-random-key-value", // long, non-placeholder + } + for _, v := range strong { + if isWeakSecret(v) { + t.Errorf("isWeakSecret(%q) = true; want false", v) + } + } +}