# Durable API keys

Configure a durable API-key store for the dashboard and mint keys that reach only the workspaces you choose.

## Prerequisites

- An Omnia install with the dashboard enabled and `dashboard.apiKeys.enabled: true` (the chart default).
- To create keys in the dashboard, a role that can manage your own API keys. The **API keys** tab on **Settings** appears only for roles that can view them.

## Choose a key store

Set `dashboard.apiKeys.store` to one of three values. The chart default is `memory`.

- `memory`: keys live in the management-api process and vanish on every restart. Each replica holds its own. Use it for development only.
- `postgres`: keys live in Postgres. Create and revoke work in the dashboard.
- `file`: keys come from a mounted Kubernetes Secret and are read-only. Creating or revoking keys in the dashboard is refused.

All stores keep only bcrypt hashes. The full key is shown once, when you create it.

management-api serves every API-key route and checks every key, from the store these values name.

1. For `postgres`, set a connection URL, or a Secret that holds it under the key `OMNIA_AUTH_API_KEYS_POSTGRES_URL`:

   ```yaml
    dashboard:
      apiKeys:
        store: postgres
        existingPostgresSecret: omnia-apikeys-postgres
    ```

    Use `postgresUrl` instead for a literal URL. With `auth.mode: builtin` and a builtin Postgres URL configured, you can omit both and the store reuses that URL. The chart fails to render if `store: postgres` has no URL from any of these sources.

    Leaving `store` at `memory` while `postgresUrl` or `existingPostgresSecret` is set switches the store to `postgres`.

2. For `file`, put the key file in a Secret in the release namespace and name it in `keysSecret`:

   ```yaml
    dashboard:
      apiKeys:
        store: file
        keysSecret: omnia-api-keys
    ```

    The chart mounts the Secret at the directory of `dashboard.apiKeys.filePath` (default `/etc/omnia/api-keys/keys.json`), so the Secret key must match the file name, `keys.json` by default. The chart fails to render if `keysSecret` is empty.

3. Apply the change:

   ```bash
    helm upgrade omnia oci://ghcr.io/altairalabs/charts/omnia --devel \
      --namespace omnia-system --reuse-values -f values.yaml
    ```

See [Dashboard authentication reference](/reference/platform/dashboard-auth/) for the key header formats and the key endpoints, and [Authentication](/explanation/security/authentication/#api-keys) for how keys are checked.

### Generating file-store keys

The file store reads `keys.json`, which holds a `keys` array. Each entry takes `id`, `userId`, `name`, `keyPrefix`, `keyHash`, `role`, `expiresAt` and `createdAt`. It also accepts optional `workspaces`, `ownerEmail` and `ownerGroups` fields.

1. From the root of the Omnia repository, with Go installed, generate a key and its entry. `--user` and `--name` are required. `--role` is `admin`, `editor` or `viewer` (default `viewer`). `--expires` is days until expiry (default `90`, `0` for never). The values below are illustrative.

   ```bash
    go run ./hack/generate-api-key --user=ci-bot --name="CI pipeline" --role=editor --expires=0
    ```

    The command prints the key once, as `omnia_sk_` followed by 43 base64url characters, and the JSON entry to add to the `keys` array. Keep the key for the client. With `--json` it prints one JSON object instead, with the key under `key` and the entry under `entry`.
2. Put the entry in `keys.json`, and add `workspaces`, `ownerEmail` or `ownerGroups` by hand if the key needs them.
3. Create the Secret:

   ```bash
    kubectl create secret generic omnia-api-keys \
      --namespace omnia-system --from-file=keys.json=./keys.json
    ```

management-api checks the mounted file at most every 5 seconds and reloads it when it changes.

## Scope a key to workspaces

By default a key carries the minting user's full access, so it can act in every workspace that user can reach. A per-key allowlist narrows that to the workspaces you name.

### Create a scoped key

1. Open **Settings**, select the **API keys** tab, and select **Create Key**.
2. Enter a name and an expiration.
3. Tick one or more workspaces under **Restrict to workspaces**.
4. Select **Create Key**, then copy the key from the **API Key Created** dialog. It cannot be shown again.

Leaving every workspace unticked creates an unrestricted key. The **Restrict to workspaces** list is hidden when you can access no workspaces.

To create the key over the API, send `POST /api/settings/api-keys` with a `workspaces` array. The values below are illustrative:

```json
{
  "name": "ci-prod-only",
  "workspaces": ["prod-workspace"]
}
```

If `workspaces` is present but is not an array of non-empty strings, the request fails with `400` and no key is created. An empty or omitted array creates an unrestricted key.

At creation the key records the owner's email (or username) and group membership. That lets it resolve the owner's per-workspace role without a live session.

A scoped key is denied outright for any workspace outside its list, and workspace listings leave those workspaces out. It never gets the platform-admin shortcut, even when its owner is a platform admin. Inside an allowed workspace it acts with the owner's own role.

A scoped key manages only keys within its own scope. It can create a key only if that key's list is inside its own, so it cannot create an unrestricted key. `GET /api/settings/api-keys` lists only the owner's keys whose lists are inside its own. `DELETE /api/settings/api-keys/{id}` revokes only those keys, and answers `404` for any other key, the same as for a key that does not exist. A session, or an unrestricted key, lists and revokes every key its owner holds.

### Deploy-profile tokens

On a workspace's **Settings** page, the **Deploy** tab has an **Export deploy profile** button. When the key store allows creation, it mints a key named `deploy-<workspace>` with `workspaces: ["<workspace>"]`, so the token reaches only that workspace.

If a key with that name already exists, the dialog asks whether to regenerate or reuse it. Regenerating revokes the old key before minting a new one. Reusing mints nothing, and the output holds a placeholder for the token you saved earlier. With the `file` store nothing is minted either, and the output holds a placeholder to fill in.

## Verify

1. List workspaces with the new key. Replace `<dashboard-url>` and `<key>`:

   ```bash
    curl -s -H "Authorization: Bearer <key>" https://<dashboard-url>/api/workspaces
    ```

    The `workspaces` array holds only the workspaces on the key's allowlist. An unrestricted key lists every workspace its owner can reach.

2. For a durable store, restart management-api and repeat the request. It should still succeed.

   ```bash
   kubectl rollout restart deployment/<management-api-deployment> --namespace omnia-system
   ```

## Troubleshooting

### The Create Key button is missing

The `file` store is read-only, and the dashboard shows "Keys managed via Kubernetes Secret" on the tab. Switch to `postgres` to create keys in the dashboard, or add the key to `keys.json` and update the Secret.

### A scoped key gets 403 on every request

The key was minted without an owner snapshot, so it resolves no workspace role, and a scoped key cannot fall back to platform admin. management-api logs a warning that names the key. Create the key again.

## Related pages

- [Dashboard authentication reference](/reference/platform/dashboard-auth/)
- [Authentication](/explanation/security/authentication/)
- [Manage workspaces](/how-to/workspaces/manage-workspaces/)