JuliusBrussee/caveman · error
%s is not set; cannot encrypt/decrypt secrets
Error message
%s is not set; cannot encrypt/decrypt secrets
What it means
secretbox.loadKey found the CAVE_LOCAL_ENCRYPTION_KEY environment variable unset/empty. In non-KMS, non-production mode this variable holds the base64 32-byte AES-256 master key used to seal local secrets; without it Encrypt/Decrypt cannot operate, so they fail fast rather than silently using a null or derived key.
Source
Thrown at shared/platform/secretbox/secretbox.go:39
"encoding/base64"
"fmt"
"os"
"strings"
"time"
"github.com/JuliusBrussee/caveman/shared/platform/kms"
"github.com/JuliusBrussee/caveman/shared/platform/runtimeenv"
)
// envKey is the name of the environment variable holding the base64-encoded
// 32-byte master key.
const envKey = "CAVE_LOCAL_ENCRYPTION_KEY"
// loadKey reads and validates the 32-byte AES key from the environment.
func loadKey() ([]byte, error) {
keyB64 := os.Getenv(envKey)
if keyB64 == "" {
return nil, fmt.Errorf("%s is not set; cannot encrypt/decrypt secrets", envKey)
}
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)View on GitHub (pinned to 27d5a3981a)
Solutions
- Generate a key and export it: openssl rand -base64 32 (then put it in the project's local env mechanism, e.g. direnv/.env, not in the repo).
- Verify with a preflight check at startup that fails fast and names the variable, instead of discovering it mid-request.
- In production, set CAVE_KMS_PROVIDER=scaleway so the KMS path is used and the local key is not required.
Example fix
# before $ ./bin/api # CAVE_LOCAL_ENCRYPTION_KEY unset -> "CAVE_LOCAL_ENCRYPTION_KEY is not set; ..." # after $ export CAVE_LOCAL_ENCRYPTION_KEY="$(openssl rand -base64 32)" $ echo 'export CAVE_LOCAL_ENCRYPTION_KEY=...' >> .envrc && direnv allow $ ./bin/api
Defensive patterns
Strategy: validation
Validate before calling
func hasLocalKey() bool { return os.Getenv("CAVE_LOCAL_ENCRYPTION_KEY") != "" }
// At startup (non-KMS, non-prod):
if !useKMSConfigured() && !runtimeenv.IsProduction() && !hasLocalKey() {
log.Fatal("CAVE_LOCAL_ENCRYPTION_KEY is not set; generate with: openssl rand -base64 32")
} Try / catch
if _, err := secretbox.Encrypt(pt); err != nil {
if strings.Contains(err.Error(), "CAVE_LOCAL_ENCRYPTION_KEY is not set") {
// fail fast with setup instructions; do not fall back to plaintext
}
} Prevention
- Put the variable in the project's env mechanism (direnv/.env, not committed) and document generation in onboarding.
- Add a startup preflight that checks the key's presence, decodability, and length before serving traffic.
- Never 'handle' this by skipping encryption — treat missing key as a hard failure.
When it happens
Trigger: Running Encrypt or Decrypt (or code that stores/reads encrypted columns, e.g. API tokens or artifact keys) in local/dev mode — useKMS() false, runtimeenv.IsProduction() false — with CAVE_LOCAL_ENCRYPTION_KEY absent from the process env (.env not loaded, wrong shell, fresh clone).
Common situations: New developer machine without the local env file; dotenv loading skipped in a test or cron context; CI job that only sets the variable for some steps; service started from a different unit-manager environment.
Related errors
- %s is not valid base64: %w
- %s must decode to exactly 32 bytes, got %d
- cave_vercel_terminal_failure
- CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM is required
- native session key random: %w
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/a5b0cd5aa56ab7ea.
Report an issue: GitHub.