JuliusBrussee/caveman · error
secretbox: production refuses legacy local ciphertext
Error message
secretbox: production refuses legacy local ciphertext
What it means
When Decrypt receives a non-KMS (legacy local AES-GCM) ciphertext in production, it refuses to decrypt it unless CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT is exactly 'true'. This guard prevents production services from silently relying on statically shared local keys, which undermines the KMS migration. It is an explicit migration-safety sentinel.
Solutions
- Re-encrypt legacy data with KMS: decrypt it once in a controlled non-production job or with the legacy flag, then store KMS envelopes.
- As a short-term bridge, set CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true in production AND ensure CAVE_SECRETBOX_KEY holds the exact original local key — then migrate off it.
- Identify which producer is still writing local ciphertext and move it to KMS encryption.
- Audit data stores for legacy envelopes (they lack the KMS envelope header detected by kms.IsEnvelope) and schedule migration.
Example fix
// before: prod reads legacy row -> secretbox: production refuses legacy local ciphertext
plaintext, err := secretbox.Decrypt(legacyEnvelope)
// after: one-time migration job (temporary opt-out + re-encrypt)
os.Setenv("CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT", "true")
pt, _ := secretbox.Decrypt(legacyEnvelope)
newEnv, _ := secretbox.Encrypt(pt) // KMS envelope now that provider is set Defensive patterns
Strategy: validation
Validate before calling
func canDecryptLegacyInProduction() error {
if runtimeenv.IsProduction() &&
!strings.EqualFold(strings.TrimSpace(os.Getenv("CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT")), "true") {
// caller should re-encrypt data instead of relying on legacy local keys
return errors.New("legacy local ciphertext present; migrate to KMS envelopes or set temporary opt-in")
}
return nil
} Try / catch
pt, err := secretbox.Decrypt(envelope)
if err != nil {
if strings.Contains(err.Error(), "refuses legacy local ciphertext") {
// route to migration job: decrypt with opt-in once, re-encrypt via KMS
return migrateToKMSEnvelope(envelope)
}
return err
} Prevention
- Plan and execute a full re-encryption migration before enabling KMS-only production mode.
- Detect legacy envelopes up front (kms.IsEnvelope == false) and batch-migrate them.
- Treat CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true as a temporary, audited bridge with an expiry.
- Ensure no producer can still write local ciphertext once the migration is declared complete.
When it happens
Trigger: Calling Decrypt (or DecryptPayloadKey/ResolveEnvironmentSecret) in production with an envelope that fails kms.IsEnvelope (i.e. legacy locally-encrypted data) while CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT is unset or not 'true'.
Common situations: Reading old database rows or config blobs encrypted before the KMS migration; restoring a backup containing legacy ciphertext into a KMS-enabled production environment; a service partially migrated where some producers still run with local keys.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- kms: probe decrypt
- secretbox: KMS decrypt
- secretbox: production requires CAVE_KMS_PROVIDER=scaleway
- kms: create request
- kms: decode envelope
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c26b563e84edc980.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/secretbox/secretbox.go:116
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)
}
ns := gcm.NonceSize()
if len(envelope) < ns {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ct := envelope[:ns], envelope[ns:]View on GitHub (pinned to 3ee70a1026)