Reference
Sign-in and secrets
Once an instance runs, with Docker, Podman or Kubernetes, this page covers letting people sign in and keeping the secrets it holds sealed, including rotating the key that seals them.
Signing in
People sign in over OpenID Connect with your identity provider, with Google, or with either: with both set up, signing in asks which, and a person is the same user whichever they pick. Register <your address>/api/auth/callback as each client's redirect URI, and set:
| Setting | What it is |
|---|---|
OIDC_ISSUER | Your provider's address. Without it or Google, nobody signs in. |
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET | The client you registered with your provider. |
OIDC_NAME | What to call your provider when signing in asks which; its host name otherwise. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | A web application's OAuth client in the Google Cloud console, for signing in with Google. |
ADMIN_EMAILS | Who is an admin, comma-separated. |
OIDC_ALLOWED_DOMAINS | Email domains anyone at may sign in, with either provider. Others need an admin to add them. |
PUBLIC_URL | The address people reach Spoletta by, when it differs from what the browser sees. |
Sealing secrets
The instance keeps secrets for others in its database: the model keys admins save, and the tokens of each person's MCP connections. With a secret key set, each is sealed (encrypted with AES-256-GCM) and bound to the row it is kept in, so a copy of the database, a backup or a dump doesn't give them away.
Make a key
At least 32 characters:
sh openssl rand -base64 32Give it to the webapp
As
SECRET_KEY, or in a file whose path is inSECRET_KEY_FILE, as with secret stores that mount secrets as files, such as the Vault Agent Injector or the Secrets Store CSI driver.Restart it
On starting, the webapp seals the secrets it kept unsealed before, and checks it can open every sealed one.
Rotating the secret key
SECRET_KEY_SECONDARY (or SECRET_KEY_SECONDARY_FILE) holds the key coming in. A webapp that has it opens secrets sealed with either key and seals new ones with the new key, but leaves the existing ones as they are, so webapps not yet restarted keep working. webapp seal-secrets then seals every secret with the new key and exits. Setting a key to nothing, or emptying its file, is the same as leaving it out.
Add the new key as the secondary, and restart every webapp
While some webapps have restarted and others haven't, what a restarted one saves, such as a new model key or a refreshed MCP token, can't be opened by the others until they restart too. One replica, or a Kubernetes
strategy: Recreate, avoids that. Let every webapp restart before the next step.Seal everything with the new key
Run
webapp seal-secretsonce, with the webapp's image and the same settings (database and both keys). It logs all N secrets are sealed with SECRET_KEY_SECONDARY. If some secret changed while it ran, it exits with 1. Run it again.Make the new key the primary
Set
SECRET_KEYto the new key andSECRET_KEY_SECONDARYto nothing, and restart again. Should a secret still be sealed with the old key, the new webapps refuse to start and the old ones go on serving: put the old key back as the secondary and repeat step 2.
Keep the old key as long as the backups taken before the rotation.
In Kubernetes
Restart the webapps with kubectl rollout restart deployment/spoletta-webapp and wait with kubectl rollout status. Step 2 is a Job:
apiVersion: batch/v1
kind: Job
metadata:
name: spoletta-seal-secrets
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: seal-secrets
image: <the webapp's image>
args: ["webapp", "seal-secrets"]
envFrom:
- secretRef:
name: spoletta
# plus DB_HOST, DB_NAME and DB_USER, as the webapp's Deployment has themkubectl apply -f seal-secrets-job.yaml
kubectl wait --for=condition=complete job/spoletta-seal-secrets --timeout=5m
kubectl logs job/spoletta-seal-secretsWhere you change the keys depends on where the webapp's settings come from:
- A Secret kept by hand: patch it.
sh NEW=$(openssl rand -base64 32) # step 1 kubectl patch secret spoletta --type merge -p "{"stringData":{"SECRET_KEY_SECONDARY":"$NEW"}}" # step 3 kubectl patch secret spoletta --type merge -p "{"stringData":{"SECRET_KEY":"$NEW","SECRET_KEY_SECONDARY":""}}" - A Secret a secrets manager syncs, such as the Vault Secrets Operator or External Secrets: change the keys in the store, since the manager would undo a patch, and wait for the Secret to sync before restarting. With Vault's KV store, for example:
sh # step 1 vault kv patch secret/spoletta SECRET_KEY_SECONDARY="$NEW" # step 3 vault kv patch secret/spoletta SECRET_KEY="$NEW" SECRET_KEY_SECONDARY="" - Keys mounted as files, as the Vault Agent Injector or the Secrets Store CSI driver do: change them in the store and point the webapp at them with
SECRET_KEY_FILEandSECRET_KEY_SECONDARY_FILE. The webapp reads them only when it starts, so restart it once the files change. The Job needs the same files mounted.
With compose
Set the keys in .env, restart with docker compose up -d webapp, and run step 2 with docker compose run --rm webapp webapp seal-secrets. With Podman, the same with podman compose.