{"record":{"id":"1f2ffc50a5da89d8","repo":"JuliusBrussee/caveman","slug":"production-refuses-default-or-empty-s","errorCode":null,"errorMessage":"production refuses default or empty %s","messagePattern":"production refuses default or empty (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/env/env.go","lineNumber":101,"sourceCode":"\tif !IsProduction() {\n\t\treturn nil\n\t}\n\tif err := validateProductionTextSecrets([]string{\"CAVE_KEY_HASH_PEPPER\"}); err != nil {\n\t\treturn err\n\t}\n\tif Bool(\"CAVE_REPLAY_ENABLED\", false) {\n\t\tif err := validateProductionTextSecrets([]string{\"CAVE_ROUTER_REPLAY_TOKEN\"}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn validateProductionPublicURL()\n}\n\nfunc validateProductionTextSecrets(names []string) error {\n\tfor _, name := range names {\n\t\tv := strings.TrimSpace(os.Getenv(name))\n\t\tif v == \"\" || v == \"generated\" || strings.Contains(strings.ToLower(v), \"changeme\") {\n\t\t\treturn fmt.Errorf(\"production refuses default or empty %s\", name)\n\t\t}\n\t\tif len(v) < 32 {\n\t\t\treturn fmt.Errorf(\"production requires %s to contain at least 32 characters\", name)\n\t\t}\n\t\tif distinctBytes([]byte(v)) < 8 {\n\t\t\treturn fmt.Errorf(\"production refuses low-diversity %s\", name)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateProductionPublicURL() error {\n\tpublicURL := strings.TrimSpace(os.Getenv(\"CAVE_PUBLIC_URL\"))\n\tu, err := url.Parse(publicURL)\n\tif err != nil || u.Scheme != \"https\" || u.Hostname() == \"\" || u.User != nil || u.RawQuery != \"\" || u.Fragment != \"\" || (u.Path != \"\" && u.Path != \"/\") {\n\t\treturn fmt.Errorf(\"production requires CAVE_PUBLIC_URL to be an HTTPS origin without credentials, path, query, or fragment\")\n\t}\n\treturn nil","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/env/env.go#L83-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before\nCAVE_JWT_SIGNING_KEY=changeme-please-replace\n\n# after\nCAVE_JWT_SIGNING_KEY=$(openssl rand -base64 48)","handlingStrategy":"validation","validationCode":"func secretIsReal(v string) bool {\n    v = strings.TrimSpace(v)\n    return v != \"\" && v != \"generated\" && !strings.Contains(strings.ToLower(v), \"changeme\")\n}","typeGuard":null,"tryCatchPattern":"if err := env.RefuseProductionDefaults(); err != nil {\n    return err // the named variable in the message is the one to replace\n}","preventionTips":["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."],"tags":["go","production","secrets","config","security"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}