JuliusBrussee/caveman · critical
secretbox: production requires CAVE_KMS_PROVIDER=scaleway
Error message
secretbox: production requires CAVE_KMS_PROVIDER=scaleway
What it means
Encrypt reached the local-key branch while runtimeenv.IsProduction() is true and no KMS provider is configured. The package enforces a hard policy: production secrets must be envelope-encrypted via Scaleway KMS (CAVE_KMS_PROVIDER=scaleway), never with a local env key — a local key in prod would concentrate all ciphertexts behind one copyable variable. This error is the guard refusing to continue.
Source
Thrown at shared/platform/secretbox/secretbox.go:64
return nil, fmt.Errorf("%s must decode to exactly 32 bytes, got %d", envKey, len(keyBytes))
}
return keyBytes, nil
}
// Encrypt seals plaintext with AES-256-GCM and a fresh random nonce, returning
// nonce(12) || ciphertext+tag as raw bytes.
func Encrypt(plaintext []byte) ([]byte, error) {
if useKMS() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wrapped, err := kms.Encrypt(ctx, plaintext)
if err != nil {
return nil, fmt.Errorf("secretbox: KMS encrypt: %w", err)
}
return wrapped, nil
}
if runtimeenv.IsProduction() {
return nil, fmt.Errorf("secretbox: production requires CAVE_KMS_PROVIDER=scaleway")
}
keyBytes, err := loadKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, fmt.Errorf("aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("aes-gcm: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("nonce entropy: %w", err)
}
// Seal appends the ciphertext+tag to nonce, so the returned slice is theView on GitHub (pinned to 27d5a3981a)
Solutions
- Set CAVE_KMS_PROVIDER=scaleway in the production environment and configure the Scaleway credentials/key the kms package needs.
- Confirm runtimeenv.IsProduction() is only true where intended — an env misclassified as production will demand KMS too.
- Add the variable to the deployment checklist/manifest diff so it cannot be dropped silently.
Example fix
# before (production deploy) RUNTIME_ENV=production # CAVE_KMS_PROVIDER missing -> "secretbox: production requires CAVE_KMS_PROVIDER=scaleway" # after RUNTIME_ENV=production CAVE_KMS_PROVIDER=scaleway SCW_ACCESS_KEY=... SCW_SECRET_KEY=... # via secret manager, not plaintext
Defensive patterns
Strategy: validation
Validate before calling
func prodCryptoConfigured() bool {
return !runtimeenv.IsProduction() || strings.EqualFold(os.Getenv("CAVE_KMS_PROVIDER"), "scaleway")
}
// fail deployment if !prodCryptoConfigured() Try / catch
if _, err := secretbox.Encrypt(pt); err != nil {
if strings.Contains(err.Error(), "production requires CAVE_KMS_PROVIDER=scaleway") {
// halt deploy/startup; set the provider + KMS credentials; do not bypass by unsetting production detection
}
} Prevention
- Add CAVE_KMS_PROVIDER=scaleway to the production manifest/deploy checklist and diff it in CI.
- Ensure runtimeenv production detection matches intent so staging isn't forced onto KMS accidentally.
- Treat this error as a policy stop, never as something to work around with a local key in prod.
When it happens
Trigger: Deploying with RUNTIME_ENV/production env detection active but CAVE_KMS_PROVIDER unset (or set to a value useKMS() doesn't recognize), so Encrypt falls through to the production check.
Common situations: Promoting a dev compose/k8s manifest to production without adding the KMS provider variable; runtimeenv detecting production (e.g. via RUNTIME_ENV=production) in a staging-like environment by accident; typo in the provider value
Related errors
- production refuses %s=true: it disables chain and hostname v
- %s: %w
- production KMS configuration: %w
- production refuses default or empty %s
- production requires %s to contain at least 32 characters
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/adc1cddc17d95a10.
Report an issue: GitHub.