Berserk Docs

Dependencies

PostgreSQL, S3 object storage, the master key and the other Kubernetes Secrets a Berserk install uses

Berserk needs PostgreSQL (v17+) for metadata and S3-compatible object storage for segment data; Hardware Sizing covers their size. Its internal secrets (the gateway's cookie key, the tokens the services present to each other, the ui's session signing key) derive from one master key, which the chart generates by default. A separate auth database and a bootstrap admin password are optional.

The Kubernetes Secrets the chart uses. For each, the chart manages it (managed: true) or you create it with kubectl (managed: false); the sections below show both. The default differs per Secret:

SecretPurposeWhen neededHelm-managed
postgres-credentialsMetadata DB connection string (meta, ui)AlwaysYes — off by default
s3-credentialsObject-store access keys (query, ingest, janitor, nursery)Always, unless EKS/IRSAYes — off by default
berserk-master-keyThe master key every internal key derives from (every service)AlwaysYes — on by default
auth-postgres-credentialsConnection string for a separate auth-stack databaseOnly with a dedicated auth DB (otherwise shares postgres-credentials)Yes — off by default
gateway-bootstrap-adminFirst admin's password (Accounts and Sign-in)Only with a pre-provisioned adminYes — when pinned

With helm install or helm upgrade, managed: true works. With Argo CD or similar, create the Secrets yourself: the chart reads whether a Secret exists, which Argo does not support. For the master key, an ignoreDifferences entry on the Secret's /data in your Argo Application also works; it keeps the first generated value.

PostgreSQL Credentials

The meta service needs a PostgreSQL (v17+) connection string.

values.yaml
global:
  postgresCredentials:
    managed: true
helm install berserk berserk/berserk \
  --namespace bzrk \
  -f values.yaml \
  --set global.postgresCredentials.databaseUrl="postgres://<user>:<password>@host:5432/berserk"

S3 Object Store

The query, ingest, janitor and nursery services use an S3-compatible object store (AWS S3, MinIO, R2, …). Every option needs the storage location in your values file:

values.yaml
global:
  storage:
    endpoint: "https://s3.eu-central-1.amazonaws.com"
    bucket: "your-berserk-bucket"
    region: "eu-central-1"
values.yaml
global:
  s3Credentials:
    managed: true
helm install berserk berserk/berserk \
  --namespace bzrk \
  -f values.yaml \
  --set global.s3Credentials.accessKeyId="AKIA..." \
  --set global.s3Credentials.secretAccessKey="secret..."

Master Key

One key is the root of every internal secret, and every service holds it: each derives the token the services present to each other, the gateway derives its cookie key, gateway, ui and permissions derive the tokens they present to each other's APIs, and the ui derives its session signing key and the key that wraps its data key. Each service derives what it needs at startup and keeps only the derived values. The Secret berserk-master-key holds:

  • master_key — required. The string's bytes are the key, exactly as given: hex, base64 and alphanumeric all work. Generate it with a cryptographic random generator; it must be at least 32 characters (global.masterKey.minLength, which you can lower to 24 for a generator that caps below 32). Base64 and alphanumeric carry about 6 bits a character, hex 4, so a hex key needs twice the length for the same strength.
  • secondary_master_key — optional, present only while a rotation is in progress.

Every service that holds the key can derive every purpose, so each is as sensitive as the key itself.

Stored integration credentials (connections, source-control grants, AI provider keys) are encrypted with a data key the ui generates on its first start and keeps in its database, wrapped under the master key (table vault_keys, one row per master key that can unwrap it). A rotation adds a wrap and never re-encrypts a row.

Any older release upgrades directly. An install that encrypted credentials under the former CREDENTIALS_ENCRYPTION_KEY keeps the chart values ui.secrets.credentialsEncryptionSecretName and ...Key for its first start with a master key: the ui adopts that key as its data key, wraps it under the master key and logs that it did. Remove the values and the Secret after that start; nothing is re-encrypted. AI provider keys that an older release stored in plaintext are encrypted at the same start. Later releases neither read the variable nor accept those values, and refuse to start if encrypted rows exist without a wrap in vault_keys.

It can be Helm-managed or external, and is Helm-managed by default: the chart generates a random value on first install and keeps it across upgrades.

The chart creates berserk-master-key with a random value:

values.yaml
global:
  masterKey:
    managed: true # default

To pin your own value instead of a random one, set global.masterKey.value. A value under minLength fails at helm template time:

values.yaml
global:
  masterKey:
    value: "<30-byte key from your generator>"
    minLength: 30

Rotating the master key

Every service presents a token derived from the key to the others, so prepare first, then take two steps, each followed by a restart of every service (editing a Secret restarts no pod):

  1. Prepare. Add the new key as secondary_master_key and restart. Every pod then accepts the new key before any pod presents it, and a gateway pod that has not restarted yet can read a session cookie a restarted one re-encrypted.
  2. Switch. Set master_key to the new key and secondary_master_key to the old one. A restarted service produces the new key's values and accepts both keys, so it still understands the pods that have not restarted yet. Signed-in browsers stay signed in: a session cookie encrypted with the old key is accepted while secondary_master_key is set, and re-encrypted with the new key the next time the browser loads a page or reconnects. Chart image links in Slack messages keep working. MCP download receipts and pending MCP approvals from before the switch stop working, and open pages reconnect once.
  3. Retire the old key. Once that rollout is complete, remove secondary_master_key and restart again. Browsers that did not visit while secondary_master_key was set sign in again, and chart image links made before step 1 stop working. The ui keeps the old key's wrap of its data key until you remove it: bin/bzrk_explore eval 'BzrkExplore.Release.retire_vault_key("<key id>")' in a ui pod, with the id the ui logged at startup (master key id=…). Keep the old key offline for as long as database backups from before the switch are kept; a restored backup needs it as secondary_master_key.

How long to keep the old key depends on why you rotate. For a routine rotation keep it for at least the session lifetime (14 days by default), which also covers the 7-day image links; then nobody signs in again. For a suspected leak keep it only for the rollout: while it is configured, anyone holding it can still derive the service tokens, and rotating does not end sessions, so delete the rows of the gateway's sessions table (schema auth by default) to end them.

Skipping step 0 means that while step 1 rolls, a restarted pod presents a token the pods that have not restarted yet do not know, and they refuse its calls until they restart.

With a Helm-managed Secret, edit the Secret in the cluster — the chart preserves both keys across upgrades — or pin global.masterKey.value and secondaryValue.

The first admin's password is not derived from the master key. Set gateway.bootstrap.adminPassword (the chart writes it to the gateway-bootstrap-admin Secret) or point gateway.bootstrap.existingSecret at a Secret you manage; see Accounts and Sign-in.

Auth Database (Optional)

The auth stack, gateway and permissions, stores its tables in PostgreSQL. By default it shares meta's database (postgres-credentials) and uses an auth schema, so it needs no extra Secret.

To run the auth stack on a separate database, give it its own connection string and turn off the shared schema with global.authDatabaseSchema: "".

values.yaml
global:
  authPostgresCredentials:
    managed: true
    secretName: "auth-postgres-credentials"
  authDatabaseSchema: "" # opt out of the shared `auth` schema
helm install berserk berserk/berserk \
  --namespace bzrk \
  -f values.yaml \
  --set global.authPostgresCredentials.databaseUrl="postgres://<user>:<password>@host:5432/berserk_auth"

Single sign-on (Optional)

Single sign-on needs no Secret and no Helm values. A cluster admin connects your identity provider on Settings → Single sign-on and enters the client_secret there, once; the gateway keeps it sealed under the master key in its database, and only the gateway reads it. See Accounts and Sign-in for the page, and Guide: Enabling Single Sign-on for the setup.

Trusted-Proxy Authentication (Optional)

An authenticating reverse proxy in front of the gateway can name the signed-in user in a request header. This needs no Secret: the gateway trusts the header on network grounds, so it must be reachable only through the proxy.

values.yaml
gateway:
  config:
    trustedProxy:
      enabled: true
      # Direct email header, SPIFFE on-behalf-of, or both:
      userHeader: X-PP-USER
      onBehalfOf:
        enabled: true
        header: on-behalf-of
        spiffePrefix: "spiffe://prod.example.net/user/"
        emailDomain: example.com

See Accounts and Sign-in → Trusted reverse proxy for every value, including directory mode.

Full Install Example

With Helm managing every Secret, one install command covers everything. The master key is generated, so it needs no flag:

values.yaml
global:
  storage:
    endpoint: "https://s3.eu-central-1.amazonaws.com"
    bucket: "your-berserk-bucket"
    region: "eu-central-1"
  s3Credentials:
    managed: true
  postgresCredentials:
    managed: true
helm repo add berserk https://berserkdb.github.io/helm-charts
helm install berserk berserk/berserk \
  --namespace bzrk \
  -f values.yaml \
  --set global.s3Credentials.accessKeyId="AKIA..." \
  --set global.s3Credentials.secretAccessKey="secret..." \
  --set global.postgresCredentials.databaseUrl="postgres://user:password@host:5432/berserk"

Later helm upgrade calls need no credential --set flags: the Secrets exist in the cluster, and the chart does not overwrite them.

See example values for minimal, Helm-managed, and EKS configurations.

On this page