§08 · Deploy & operate

Control-plane config & secrets

Every environment variable the Exo backend reads, which ones are mandatory, and how to generate the two secrets that gate startup.

The backend is configured through environment variables: typically a ConfigMap for the non-secret ones and a Secret for the rest. Two are mandatory and the process fails fast without them. There are no production-safe defaults for a signing key or an encryption key.

Environment variables

VariableRequiredDefaultPurposeExample
EXO_ENCRYPTION_KEY✓ yes(none)AES-256 root key, exactly 32 bytes9f2a4c6e8b0d1f3a5c7e9b1d3f5a7c9e
EXO_JWT_SECRET✓ yesrequired — no defaultHS256 key for user + OAuth tokensxK9wL2mN…
EXO_DATABASE_DSN✓ prodlocalhost dev DSNPostgres connection stringpostgres://exo:…@db.acme.internal:5432/exo
EXO_PORTno9092HTTP listen port9092
EXO_BASE_URLnohttp://localhost:9092Public URL (email links, OAuth cb)https://exo.acme.com
EXO_JWT_EXPIRYno24hUser JWT lifetime (Go duration)24h
EXO_CAST_STORAGE_DIRno/var/lib/exo/castsSession-recording dir (writable vol)/var/lib/exo/casts
EXO_CLAUDE_API_KEYno(empty)base64 Anthropic key, AI fallbackc2stYW50LWFwaTAzL…

EXO_ENCRYPTION_KEY

Root of the envelope-encryption keyring. It wraps the key-encryption key, which in turn encrypts every sensitive column at rest: integration credentials, identity-provider secrets, log-sink configs, tool permissions, session payloads. A database dump is useless without it.

EXO_JWT_SECRET

Signs every user JWT and every short-lived OAuth access token (HS256). Rotating it invalidates all active sessions and issued tokens immediately, so treat it like a database password: vault it, rotate on a schedule, never commit it. There is no default: EXO_JWT_SECRET is required, and the well-known value changeme-secret-key is rejected at boot so a forgeable instance can't start.

EXO_CLAUDE_API_KEY

Optional. A platform-default Anthropic key used by the "Generate with AI" features (skill generation) when a tenant hasn't configured its own provider. It must be base64-encoded in the Secret; the backend decodes it at read time. Leave it blank to disable the fallback. In production, prefer Claude Workload Identity Federation over a static key.

terminal· bash
printf '%s' 'sk-ant-...' | base64

Generating secrets

If you use exo-install, the JWT secret, encryption key, and (with the bundled Postgres) the database password are generated for you. With the Helm chart you create the key Secret yourself and the chart reads it by reference (it won't install otherwise). For manual installs:

terminal· bash
EXO_ENCRYPTION_KEY="$(openssl rand -hex 16)"
EXO_JWT_SECRET="$(openssl rand -base64 48)"
kubectl -n exo create secret generic exo-secrets \
--from-literal=EXO_ENCRYPTION_KEY="$EXO_ENCRYPTION_KEY" \
--from-literal=EXO_JWT_SECRET="$EXO_JWT_SECRET" \
--from-literal=EXO_DATABASE_DSN="host=db user=exo dbname=exo sslmode=require"