SpolettaDocs

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:

SettingWhat it is
OIDC_ISSUERYour provider's address. Without it or Google, nobody signs in.
OIDC_CLIENT_ID, OIDC_CLIENT_SECRETThe client you registered with your provider.
OIDC_NAMEWhat to call your provider when signing in asks which; its host name otherwise.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRETA web application's OAuth client in the Google Cloud console, for signing in with Google.
ADMIN_EMAILSWho is an admin, comma-separated.
OIDC_ALLOWED_DOMAINSEmail domains anyone at may sign in, with either provider. Others need an admin to add them.
PUBLIC_URLThe 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.

  1. Make a key

    At least 32 characters:

    sh
    openssl rand -base64 32
  2. Give it to the webapp

    As SECRET_KEY, or in a file whose path is in SECRET_KEY_FILE, as with secret stores that mount secrets as files, such as the Vault Agent Injector or the Secrets Store CSI driver.

  3. 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.

  1. 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.

  2. Seal everything with the new key

    Run webapp seal-secrets once, 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.

  3. Make the new key the primary

    Set SECRET_KEY to the new key and SECRET_KEY_SECONDARY to 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:

seal-secrets-job.yamlyaml
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 them
sh
kubectl apply -f seal-secrets-job.yaml
kubectl wait --for=condition=complete job/spoletta-seal-secrets --timeout=5m
kubectl logs job/spoletta-seal-secrets

Where 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_FILE and SECRET_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.

Spoletta · spoletta.comBusiness processes, drawn and run.