JuliusBrussee/caveman · critical
secretbox: KMS decrypt
Error message
secretbox: KMS decrypt: %w
What it means
secretbox.Decrypt first checks kms.IsEnvelope(envelope); if the ciphertext is a KMS envelope, it calls kms.Decrypt with a 10s timeout and wraps any failure as 'secretbox: KMS decrypt'. The underlying error (bad credentials, envelope from a different key/region, tampering, or timeout) is preserved via %w. Decryption fails closed — there is no local fallback for KMS envelopes.
Solutions
- Unwrap the error to see the KMS cause (permission denied, not found, timeout, etc.).
- Confirm CAVE_KMS_PROVIDER credentials and key configuration match the key that produced the envelope (same key ID/region).
- Check IAM/KMS ACLs: the runtime identity must have decrypt rights on the key.
- If envelopes were created under an old key, decrypt with the old key configured or re-encrypt data under the new key.
- Retry after transient KMS outages; check endpoint reachability for network causes.
Example fix
// before: envelope from staging key decrypted in prod -> secretbox: KMS decrypt: not found CAVE_KMS_KEY_ID=prod-key // after: point decryptor at the key that wrapped the payload, or migrate data CAVE_KMS_KEY_ID=staging-key # or re-encrypt staging data under prod-key
Defensive patterns
Strategy: try-catch
Validate before calling
if kms.IsEnvelope(envelope) {
if os.Getenv("CAVE_KMS_PROVIDER") == "" {
return errors.New("KMS envelope encountered but no CAVE_KMS_PROVIDER configured")
}
} Try / catch
pt, err := secretbox.Decrypt(envelope)
if err != nil {
if strings.Contains(err.Error(), "KMS decrypt") {
switch {
case errors.Is(err, context.DeadlineExceeded):
return retry(envelope) // transient latency
default:
// likely key/permission mismatch: do not retry blindly
return fmt.Errorf("cannot decrypt envelope: %w", err)
}
}
return err
} Prevention
- Track which KMS key encrypted each record (key ID in metadata) and configure the matching key before decrypting.
- Grant the runtime identity decrypt permission on every key that produced live envelopes.
- Rotate credentials with coordinated redeployment, not in-place secret edits.
- Run a decrypt self-test on a known envelope in startup health checks.
When it happens
Trigger: Calling Decrypt (directly or via DecryptPayloadKey / ResolveEnvironmentSecret) with a KMS-format envelope when kms.Decrypt errors: credentials lacking decrypt permission on the key, envelope produced in a different KMS key/region than the one configured, KMS outage, or >10s latency.
Common situations: Data encrypted in one environment/region decrypted in another with a different key configured; IAM policy revoked the runtime identity's kms-decrypt right; Scaleway credentials rotated without redeploy; cross-account key usage not granted.
Related errors
- kms: probe decrypt
- secretbox: payload KMS encrypt
- secretbox: production refuses legacy local ciphertext
- aes cipher
- envelope: unwrap data key
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/5f758d304a9404af.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/secretbox/secretbox.go:110
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wrapped, err := kms.EncryptPayload(ctx, plaintext)
if err != nil {
return nil, fmt.Errorf("secretbox: payload KMS encrypt: %w", err)
}
return wrapped, nil
}
return Encrypt(plaintext)
}
// Decrypt reverses Encrypt: it expects nonce(12) || ciphertext+tag.
func Decrypt(envelope []byte) ([]byte, error) {
if kms.IsEnvelope(envelope) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
plaintext, err := kms.Decrypt(ctx, envelope)
if err != nil {
return nil, fmt.Errorf("secretbox: KMS decrypt: %w", err)
}
return plaintext, nil
}
if runtimeenv.IsProduction() &&
!strings.EqualFold(strings.TrimSpace(os.Getenv("CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT")), "true") {
return nil, fmt.Errorf("secretbox: production refuses legacy local ciphertext")
}
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)View on GitHub (pinned to 3ee70a1026)