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

  1. Replace the secret with at least 32 characters of random material: openssl rand -base64 48.
  2. If a key is derived from a passphrase, feed the passphrase through a KDF at rest and store the long output instead.
  3. Check the secret was not truncated in transit - compare lengths between source and process env.
  4. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/8b4edb9b823ac1a5. Report an issue: GitHub.