JuliusBrussee/caveman · error

secretbox: payload KMS encrypt: %w

Error message

secretbox: payload KMS encrypt: %w

What it means

EncryptPayloadKey — which wraps an artifact data-encryption key (DEK) under the payload KEK — took the KMS branch and kms.EncryptPayload failed within its 10s context. Distinct from the general secret path: it uses the dedicated payload key in the KMS rather than the general-purpose key, and local development falls back to the same AES-GCM envelope (so this error is production/KMS-only). The %w wrap preserves the KMS cause.

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 27d5a3981a)

Solutions

  1. Verify the payload key ID/region configured for kms.EncryptPayload exists and the credentials can use it (a quick test wrap via the Scaleway CLI or a probe call).
  2. Inspect the wrapped cause for HTTP status: 403 = IAM, 404 = wrong key/region, 429 = throttle, timeout = network/latency.
  3. For latency-sensitive artifact flows, cache per-process what can be cached (or batch wraps) and retry transient causes with backoff at the caller.

Example fix

// before
wrapped, err := secretbox.EncryptPayloadKey(dek)
if err != nil { return err } // "secretbox: payload KMS encrypt: ..."

// after
var wrapped []byte
if err := retry(3, 250*time.Millisecond, func() error {
    var e error
    wrapped, e = secretbox.EncryptPayloadKey(dek)
    return e
}); err != nil {
    return fmt.Errorf("wrap artifact DEK: %w", err)
}
Defensive patterns

Strategy: retry

Validate before calling

// Startup probe for the payload KEK specifically:
func probePayloadKEK(ctx context.Context) error {
    wrapped, err := kms.EncryptPayload(ctx, []byte("probe"))
    if err != nil { return err }
    _, err = kms.Decrypt(ctx, wrapped)
    return err
}

Try / catch

var wrapped []byte
err := retry(3, 250*time.Millisecond, func() error {
    var e error
    wrapped, e = secretbox.EncryptPayloadKey(dek)
    return e
})
if err != nil { return fmt.Errorf("wrap DEK: %w", err) } // retry only network/429 causes

Prevention

When it happens

Trigger: Uploading/storing an encrypted artifact in KMS mode: the payload-specific key reference is wrong or disabled, Scaleway credentials lack permission on that key, the KMS API is unreachable from the service, or the wrap call exceeded the 10s deadline.

Common situations: The payload KEK was created in one Scaleway project/region and the service points at another; IAM policy grants access to the general key but not the payload key; regional KMS outage; per-artifact upload volume hitting KMS throttles.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/0e79526eac2ef257. Report an issue: GitHub.