JuliusBrussee/caveman · critical

secretbox: KMS encrypt

Error message

secretbox: KMS encrypt: %w

What it means

secretbox.Encrypt detected that KMS-based encryption is enabled (CAVE_KMS_PROVIDER is set) and delegated the encryption to the configured KMS provider, but the kms.Encrypt call failed. The underlying KMS error (network failure, bad credentials, misconfigured key) is wrapped with %w so the cause is preserved. This error means the envelope could not be produced at all.

Solutions

  1. Check the wrapped cause: print err with %v or errors.Unwrap to see the underlying KMS failure (auth vs network vs timeout).
  2. Verify CAVE_KMS_PROVIDER and the provider credential env vars (e.g. Scaleway access key, secret key, region) are present and current.
  3. Test network egress to the KMS endpoint from the host/container; confirm no firewall or VPC rule blocks it.
  4. If credentials were rotated, redeploy with the new values and re-run the failing operation.
  5. For local development where KMS is unnecessary, unset CAVE_KMS_PROVIDER so secretbox falls back to local AES-GCM (not allowed in production).

Example fix

// before (missing/rotated credentials)
os.Setenv("CAVE_KMS_PROVIDER", "scaleway")
// encrypted, err := secretbox.Encrypt(plaintext) -> secretbox: KMS encrypt: ...

// after (valid creds exported, or local mode outside production)
os.Setenv("CAVE_KMS_ACCESS_KEY", scwAccessKey)
os.Setenv("CAVE_KMS_SECRET_KEY", scwSecretKey)
// or, in dev only: os.Unsetenv("CAVE_KMS_PROVIDER")
Defensive patterns

Strategy: try-catch

Validate before calling

if os.Getenv("CAVE_KMS_PROVIDER") != "" {
    for _, v := range []string{"CAVE_KMS_ACCESS_KEY", "CAVE_KMS_SECRET_KEY"} {
        if os.Getenv(v) == "" {
            return fmt.Errorf("%s must be set when CAVE_KMS_PROVIDER is configured", v)
        }
    }
}

Try / catch

wrapped, err := secretbox.Encrypt(plaintext)
if err != nil {
    var kmsErr *kms.Error
    if errors.As(err, &kmsErr) {
        // alert: KMS unavailable; fail closed, do not fall back to local keys
        return fmt.Errorf("encrypt unavailable: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling secretbox.Encrypt (directly or via EncryptPayloadKey falling through) while useKMS() is true and kms.Encrypt returns a non-nil error, e.g. expired Scaleway credentials, unreachable KMS endpoint, or an invalid CAVE_KMS_* key configuration. The call has a hard 10s context timeout, so KMS latency over 10 seconds also produces this.

Common situations: Deployed environment has CAVE_KMS_PROVIDER=scaleway but the access/secret key env vars are wrong or rotated; KMS API outage or network egress blocked from the container; KMS request exceeding the 10-second timeout; wrong KMS region/key ID configured.

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/5a86c7edc0f22ba4. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/secretbox/secretbox.go:59

	keyBytes, err := base64.StdEncoding.DecodeString(keyB64)
	if err != nil {
		return nil, fmt.Errorf("%s is not valid base64: %w", envKey, err)
	}
	if len(keyBytes) != 32 {
		return nil, fmt.Errorf("%s must decode to exactly 32 bytes, got %d", envKey, len(keyBytes))
	}
	return keyBytes, nil
}

// Encrypt seals plaintext with AES-256-GCM and a fresh random nonce, returning
// nonce(12) || ciphertext+tag as raw bytes.
func Encrypt(plaintext []byte) ([]byte, error) {
	if useKMS() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		wrapped, err := kms.Encrypt(ctx, plaintext)
		if err != nil {
			return nil, fmt.Errorf("secretbox: KMS encrypt: %w", err)
		}
		return wrapped, nil
	}
	if runtimeenv.IsProduction() {
		return nil, fmt.Errorf("secretbox: production requires CAVE_KMS_PROVIDER=scaleway")
	}
	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)