JuliusBrussee/caveman · critical
secretbox: payload KMS encrypt
Error message
secretbox: payload KMS encrypt: %w
What it means
EncryptPayloadKey encrypts data-encryption keys. When useKMS() is true it delegates to kms.EncryptPayload with a 10s timeout and wraps any KMS failure as 'secretbox: payload KMS encrypt'. This is distinct from the master Encrypt path (kms.Encrypt) and means the payload-key envelope could not be produced. Note it wraps the error rather than falling back to local encryption, so a KMS outage fails closed.
Solutions
- Inspect the wrapped error (errors.Unwrap) to identify auth vs network vs timeout cause.
- Verify the payload-key-specific credentials and key ID/region env vars are current and match the Scaleway console.
- Check KMS service status and retry after transient outages; the errors are intentionally not masked by a local fallback.
- Confirm network egress from the workload to the Scaleway KMS endpoint; add NAT/proxy rules if missing.
- For requests that hit the 10s deadline, check KMS latency and consider the configured timeout budget.
Example fix
// before: rotated key not redeployed -> secretbox: payload KMS encrypt: 401 unauthorized
// process started before key rotation
// after: reload credentials on rotation and restart/re-auth
if err := kms.RefreshCredentials(ctx); err != nil { log.Fatal(err) }
wrapped, err := secretbox.EncryptPayloadKey(dek) Defensive patterns
Strategy: try-catch
Validate before calling
if os.Getenv("CAVE_KMS_PROVIDER") == "scaleway" {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := kms.Ping(ctx); err != nil { // or a tiny EncryptPayload round trip
return fmt.Errorf("payload KMS not reachable at startup: %w", err)
}
} Try / catch
wrapped, err := secretbox.EncryptPayloadKey(dek)
if err != nil {
if kmsErr := (*kms.Error)(nil); errors.As(err, &kmsErr) {
// fail closed: never fall back to local key encryption for DEKs
return fmt.Errorf("payload key wrapping failed, refusing local fallback: %w", err)
}
if errors.Is(err, context.DeadlineExceeded) {
// retry with backoff for transient latency
return retryWithBackoff(dek)
}
return err
} Prevention
- Health-check the payload-key KMS path at startup with a real wrap/unwrap of a test DEK.
- Redeploy or hot-reload credentials after any key rotation.
- Alert on KMS error-rate spikes; they will fail closed by design.
- Keep key ID/region env config consistent across producers and consumers of payload keys.
When it happens
Trigger: Calling EncryptPayloadKey while CAVE_KMS_PROVIDER is set and kms.EncryptPayload fails: bad or rotated Scaleway payload-key credentials, wrong key ID/region, KMS endpoint unreachable, or the 10s context deadline exceeded.
Common situations: KMS outage or rate limiting during a deploy stampede; payload-key rotated in the Scaleway console while running processes still hold old credentials; region mismatch between the configured key and the API endpoint; egress blocked in a private subnet without NAT to the KMS API.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/0e79526eac2ef257.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/secretbox/secretbox.go:96
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 the
// full nonce||ciphertext envelope.
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
// EncryptPayloadKey wraps an artifact data-encryption key. Production uses the
// dedicated payload KEK; local development retains the same AES-GCM envelope as
// other local secrets.
func EncryptPayloadKey(plaintext []byte) ([]byte, error) {
if useKMS() {
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() &&View on GitHub (pinned to 3ee70a1026)