JuliusBrussee/caveman · critical
production refuses default or empty %s
Error message
production refuses default or empty %s
What it means
Produced by validateProductionTextSecrets (env.go:101) inside the production gates: a checked secret is empty, equals the literal 'generated', or contains 'changeme' (case-insensitive substring). These are the known placeholder values shipped in templates/examples, so in production they are refused outright. The variable name is included in the message.
Source
Thrown at shared/platform/env/env.go:101
if !IsProduction() {
return nil
}
if err := validateProductionTextSecrets([]string{"CAVE_KEY_HASH_PEPPER"}); err != nil {
return err
}
if Bool("CAVE_REPLAY_ENABLED", false) {
if err := validateProductionTextSecrets([]string{"CAVE_ROUTER_REPLAY_TOKEN"}); err != nil {
return err
}
}
return validateProductionPublicURL()
}
func validateProductionTextSecrets(names []string) error {
for _, name := range names {
v := strings.TrimSpace(os.Getenv(name))
if v == "" || v == "generated" || strings.Contains(strings.ToLower(v), "changeme") {
return fmt.Errorf("production refuses default or empty %s", name)
}
if len(v) < 32 {
return fmt.Errorf("production requires %s to contain at least 32 characters", name)
}
if distinctBytes([]byte(v)) < 8 {
return fmt.Errorf("production refuses low-diversity %s", name)
}
}
return nil
}
func validateProductionPublicURL() error {
publicURL := strings.TrimSpace(os.Getenv("CAVE_PUBLIC_URL"))
u, err := url.Parse(publicURL)
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") {
return fmt.Errorf("production requires CAVE_PUBLIC_URL to be an HTTPS origin without credentials, path, query, or fragment")
}
return nilView on GitHub (pinned to 27d5a3981a)
Solutions
- Generate a real secret: openssl rand -base64 48, and set it for the named variable.
- Search the value for 'changeme' case-insensitively - even documentation-style text trips the check.
- If a generator wrote 'generated', fix the generator to output randomness, then re-set the variable.
- Rotate the deployment so the new value is actually injected (helm upgrade / compose up with the updated secret).
Example fix
# before CAVE_JWT_SIGNING_KEY=changeme-please-replace # after CAVE_JWT_SIGNING_KEY=$(openssl rand -base64 48)
Defensive patterns
Strategy: validation
Validate before calling
func secretIsReal(v string) bool {
v = strings.TrimSpace(v)
return v != "" && v != "generated" && !strings.Contains(strings.ToLower(v), "changeme")
} Try / catch
if err := env.RefuseProductionDefaults(); err != nil {
return err // the named variable in the message is the one to replace
} Prevention
- Generate all secrets with openssl rand -base64 48 at provision time.
- Add a lint rule to secret manifests forbidding the strings 'generated' and 'changeme'.
- Never promote .env.example values; keep a separate prod secret source.
When it happens
Trigger: CAVE_ENV is production and one of the checked secrets (e.g. CAVE_KEY_HASH_PEPPER, CAVE_JWT_SIGNING_KEY, CAVE_BOOTSTRAP_TOKEN, or CAVE_ROUTER_REPLAY_TOKEN when replay is enabled) is empty, 'generated', or contains 'changeme' anywhere in its lowercase form.
Common situations: Copying .env.example to production with placeholder values intact; a generator script that writes the literal string 'generated'; a token containing 'ChangeMe-InProd' as documentation; secrets stripped by a security scanner that replaced values with placeholders.
Related errors
- production requires %s to contain at least 32 characters
- production requires an https:// CLICKHOUSE_URL (TLS only); C
- production KMS configuration: %w
- production refuses low-diversity %s
- production requires CAVE_PUBLIC_URL to be an HTTPS origin wi
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/1f2ffc50a5da89d8.
Report an issue: GitHub.