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

  1. Unwrap the error to see the KMS cause (permission denied, not found, timeout, etc.).
  2. Confirm CAVE_KMS_PROVIDER credentials and key configuration match the key that produced the envelope (same key ID/region).
  3. Check IAM/KMS ACLs: the runtime identity must have decrypt rights on the key.
  4. If envelopes were created under an old key, decrypt with the old key configured or re-encrypt data under the new key.
  5. 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

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


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)