{"record":{"id":"a5b0cd5aa56ab7ea","repo":"JuliusBrussee/caveman","slug":"s-is-not-set-cannot-encrypt-decrypt-secrets","errorCode":null,"errorMessage":"%s is not set; cannot encrypt/decrypt secrets","messagePattern":"(.+?) is not set; cannot encrypt/decrypt secrets","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/secretbox/secretbox.go","lineNumber":39,"sourceCode":"\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/JuliusBrussee/caveman/shared/platform/kms\"\n\t\"github.com/JuliusBrussee/caveman/shared/platform/runtimeenv\"\n)\n\n// envKey is the name of the environment variable holding the base64-encoded\n// 32-byte master key.\nconst envKey = \"CAVE_LOCAL_ENCRYPTION_KEY\"\n\n// loadKey reads and validates the 32-byte AES key from the environment.\nfunc loadKey() ([]byte, error) {\n\tkeyB64 := os.Getenv(envKey)\n\tif keyB64 == \"\" {\n\t\treturn nil, fmt.Errorf(\"%s is not set; cannot encrypt/decrypt secrets\", envKey)\n\t}\n\tkeyBytes, err := base64.StdEncoding.DecodeString(keyB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s is not valid base64: %w\", envKey, err)\n\t}\n\tif len(keyBytes) != 32 {\n\t\treturn nil, fmt.Errorf(\"%s must decode to exactly 32 bytes, got %d\", envKey, len(keyBytes))\n\t}\n\treturn keyBytes, nil\n}\n\n// Encrypt seals plaintext with AES-256-GCM and a fresh random nonce, returning\n// nonce(12) || ciphertext+tag as raw bytes.\nfunc Encrypt(plaintext []byte) ([]byte, error) {\n\tif useKMS() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\t\twrapped, err := kms.Encrypt(ctx, plaintext)","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/secretbox/secretbox.go#L21-L57","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"# before\n$ ./bin/api   # CAVE_LOCAL_ENCRYPTION_KEY unset -> \"CAVE_LOCAL_ENCRYPTION_KEY is not set; ...\"\n\n# after\n$ export CAVE_LOCAL_ENCRYPTION_KEY=\"$(openssl rand -base64 32)\"\n$ echo 'export CAVE_LOCAL_ENCRYPTION_KEY=...' >> .envrc && direnv allow\n$ ./bin/api","handlingStrategy":"validation","validationCode":"func hasLocalKey() bool { return os.Getenv(\"CAVE_LOCAL_ENCRYPTION_KEY\") != \"\" }\n\n// At startup (non-KMS, non-prod):\nif !useKMSConfigured() && !runtimeenv.IsProduction() && !hasLocalKey() {\n    log.Fatal(\"CAVE_LOCAL_ENCRYPTION_KEY is not set; generate with: openssl rand -base64 32\")\n}","typeGuard":null,"tryCatchPattern":"if _, err := secretbox.Encrypt(pt); err != nil {\n    if strings.Contains(err.Error(), \"CAVE_LOCAL_ENCRYPTION_KEY is not set\") {\n        // fail fast with setup instructions; do not fall back to plaintext\n    }\n}","preventionTips":["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."],"tags":["secrets","environment","configuration","crypto"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}