JuliusBrussee/caveman · critical

secretbox: production requires CAVE_KMS_PROVIDER=scaleway

Error message

secretbox: production requires CAVE_KMS_PROVIDER=scaleway

What it means

Encrypt reached the local-key branch while runtimeenv.IsProduction() is true and no KMS provider is configured. The package enforces a hard policy: production secrets must be envelope-encrypted via Scaleway KMS (CAVE_KMS_PROVIDER=scaleway), never with a local env key — a local key in prod would concentrate all ciphertexts behind one copyable variable. This error is the guard refusing to continue.

Source

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

		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)
	}
	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

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set CAVE_KMS_PROVIDER=scaleway in the production environment and configure the Scaleway credentials/key the kms package needs.
  2. Confirm runtimeenv.IsProduction() is only true where intended — an env misclassified as production will demand KMS too.
  3. Add the variable to the deployment checklist/manifest diff so it cannot be dropped silently.

Example fix

# before (production deploy)
RUNTIME_ENV=production
# CAVE_KMS_PROVIDER missing -> "secretbox: production requires CAVE_KMS_PROVIDER=scaleway"

# after
RUNTIME_ENV=production
CAVE_KMS_PROVIDER=scaleway
SCW_ACCESS_KEY=...
SCW_SECRET_KEY=... # via secret manager, not plaintext
Defensive patterns

Strategy: validation

Validate before calling

func prodCryptoConfigured() bool {
    return !runtimeenv.IsProduction() || strings.EqualFold(os.Getenv("CAVE_KMS_PROVIDER"), "scaleway")
}
// fail deployment if !prodCryptoConfigured()

Try / catch

if _, err := secretbox.Encrypt(pt); err != nil {
    if strings.Contains(err.Error(), "production requires CAVE_KMS_PROVIDER=scaleway") {
        // halt deploy/startup; set the provider + KMS credentials; do not bypass by unsetting production detection
    }
}

Prevention

When it happens

Trigger: Deploying with RUNTIME_ENV/production env detection active but CAVE_KMS_PROVIDER unset (or set to a value useKMS() doesn't recognize), so Encrypt falls through to the production check.

Common situations: Promoting a dev compose/k8s manifest to production without adding the KMS provider variable; runtimeenv detecting production (e.g. via RUNTIME_ENV=production) in a staging-like environment by accident; typo in the provider value

Related errors


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