JuliusBrussee/caveman · critical

secretbox: production requires CAVE_KMS_PROVIDER=scaleway

Error message

secretbox: production requires CAVE_KMS_PROVIDER=scaleway

What it means

secretbox.Encrypt refuses to use local AES-GCM key encryption when runtimeenv.IsProduction() is true, because a shared local key is not acceptable key management in production. The library mandates KMS envelope encryption via CAVE_KMS_PROVIDER=scaleway in production, and throws this sentinel error otherwise. It is a policy guard, not an operational failure.

Solutions

  1. Set CAVE_KMS_PROVIDER=scaleway in the production environment before starting the process.
  2. Ensure the Scaleway KMS credentials (access key, secret key, key ID) are also exported so kms.Encrypt can run.
  3. If this is genuinely not production, fix the environment marker that makes runtimeenv.IsProduction() return true (e.g. CAVE_ENV/NODE_ENV-like variable).
  4. Never bypass this in production; do not attempt to ship a local key instead.

Example fix

// before
// deployment manifest without KMS config -> secretbox: production requires CAVE_KMS_PROVIDER=scaleway

// after (k8s env block)
env:
  - name: CAVE_KMS_PROVIDER
    value: "scaleway"
  - name: CAVE_KMS_ACCESS_KEY
    valueFrom:
      secretKeyRef: { name: cave-kms, key: access-key }
Defensive patterns

Strategy: validation

Validate before calling

func checkSecretboxConfig() error {
    if runtimeenv.IsProduction() && os.Getenv("CAVE_KMS_PROVIDER") != "scaleway" {
        return errors.New("production requires CAVE_KMS_PROVIDER=scaleway")
    }
    return nil
}
// call at process startup, before any encryption
if err := checkSecretboxConfig(); err != nil { log.Fatal(err) }

Prevention

When it happens

Trigger: Calling secretbox.Encrypt in a process where runtimeenv.IsProduction() is true (production environment marker set) while useKMS() is false, i.e. CAVE_KMS_PROVIDER is unset or not 'scaleway'.

Common situations: Running a production build locally with a production env flag exported; Docker/K8s deployment missing the CAVE_KMS_PROVIDER=scaleway env var; typo like CAVE_KMS_PROVIDER=Scaleway handled elsewhere but provider simply not configured; staging-config accidentally applied to production.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/adc1cddc17d95a10. Report an issue: GitHub.

Appendix: 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 3ee70a1026)