JuliusBrussee/caveman · critical
secretbox: production requires CAVE_KMS_PROVIDER=scaleway
Error message
secretbox: production requires CAVE_KMS_PROVIDER=scaleway
What it means
secretbox.Encrypt refuses to use local AES-GCM key encryption when runtimeenv.IsProduction() is true, because a shared local key is not acceptable key management in production. The library mandates KMS envelope encryption via CAVE_KMS_PROVIDER=scaleway in production, and throws this sentinel error otherwise. It is a policy guard, not an operational failure.
Solutions
- Set CAVE_KMS_PROVIDER=scaleway in the production environment before starting the process.
- Ensure the Scaleway KMS credentials (access key, secret key, key ID) are also exported so kms.Encrypt can run.
- If this is genuinely not production, fix the environment marker that makes runtimeenv.IsProduction() return true (e.g. CAVE_ENV/NODE_ENV-like variable).
- Never bypass this in production; do not attempt to ship a local key instead.
Example fix
// before
// deployment manifest without KMS config -> secretbox: production requires CAVE_KMS_PROVIDER=scaleway
// after (k8s env block)
env:
- name: CAVE_KMS_PROVIDER
value: "scaleway"
- name: CAVE_KMS_ACCESS_KEY
valueFrom:
secretKeyRef: { name: cave-kms, key: access-key } Defensive patterns
Strategy: validation
Validate before calling
func checkSecretboxConfig() error {
if runtimeenv.IsProduction() && os.Getenv("CAVE_KMS_PROVIDER") != "scaleway" {
return errors.New("production requires CAVE_KMS_PROVIDER=scaleway")
}
return nil
}
// call at process startup, before any encryption
if err := checkSecretboxConfig(); err != nil { log.Fatal(err) } Prevention
- Validate the full secretbox/KMS config in an init or health check that runs before any data path.
- Encode CAVE_KMS_PROVIDER=scaleway in the production deployment template/manifest, not per-developer env files.
- Add a CI check that renders the production config and asserts the KMS provider is set.
- Document that local-key encryption is dev-only and must never be configured in prod.
When it happens
Trigger: Calling secretbox.Encrypt in a process where runtimeenv.IsProduction() is true (production environment marker set) while useKMS() is false, i.e. CAVE_KMS_PROVIDER is unset or not 'scaleway'.
Common situations: Running a production build locally with a production env flag exported; Docker/K8s deployment missing the CAVE_KMS_PROVIDER=scaleway env var; typo like CAVE_KMS_PROVIDER=Scaleway handled elsewhere but provider simply not configured; staging-config accidentally applied to production.
Understand the failure class
Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.
Related errors
- aes cipher
- kms: plaintext exceeds
- kms: probe encrypt
- production KMS configuration
- secretbox: KMS encrypt
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/adc1cddc17d95a10.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)