JuliusBrussee/caveman · critical
production requires %s to contain at least 32 characters
Error message
production requires %s to contain at least 32 characters
What it means
Produced by validateProductionTextSecrets (env.go:104) during the production gates: the secret passed the empty/placeholder check but is shorter than 32 characters. The library enforces a minimum entropy budget by raw length; a short pepper or signing key is brute-forceable and therefore refuses boot in production. The variable name is included.
Source
Thrown at shared/platform/env/env.go:104
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 nil
}
func distinctBytes(value []byte) int {View on GitHub (pinned to 27d5a3981a)
Solutions
- Replace the secret with at least 32 characters of random material: openssl rand -base64 48.
- If a key is derived from a passphrase, feed the passphrase through a KDF at rest and store the long output instead.
- Check the secret was not truncated in transit - compare lengths between source and process env.
- Re-run the boot validation after updating the injection.
Example fix
# before CAVE_KEY_HASH_PEPPER=s3cr3t # 6 chars # after CAVE_KEY_HASH_PEPPER=$(openssl rand -base64 48)
Defensive patterns
Strategy: validation
Validate before calling
if len(strings.TrimSpace(secret)) < 32 {
return fmt.Errorf("secret too short: need >=32 chars")
} Try / catch
if err := env.RefuseProductionDefaults(); err != nil {
return err // regenerate the named secret with more material
} Prevention
- Standardize on openssl rand -base64 48 output for every text secret.
- Add secret-length assertions to your provisioning scripts.
- Check secret lengths after secret-manager injection, since some managers truncate.
When it happens
Trigger: CAVE_ENV is production and a checked secret has fewer than 32 characters after trimming (31 chars fails; 32 passes this check, then the diversity check applies).
Common situations: A human-chosen password phrase like 'cave-prod-2024' used as a pepper; short dev tokens promoted to prod; base64 of a 16-byte value (22-24 chars); truncation by a secret manager field limit.
Related errors
- production refuses default or empty %s
- 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/8b4edb9b823ac1a5.
Report an issue: GitHub.