> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyborg.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Tenancy & RBAC

CyborgDB Service v0.17 ships with an **operator-side multi-tenancy model** built on cryptographic isolation: each index is wrapped by its own key, and per-user API keys hold their own wrapped data-encryption keys (DEKs). Permissions aren't stored as a policy blob — the **set of wrapped DEKs a user holds *is* their permission set**, enforced by the encrypted-index engine on every request.

This page is the operator playbook: how to enable RBAC, mint and revoke users, and the constraints to be aware of.

## Modes

| Mode                     | Enabled by                        | Who can call what                                                                                                                                                                                                                    |
| ------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Single-key (default)** | `CYBORGDB_SERVICE_ROOT_KEY` unset | One `CYBORGDB_API_KEY` has full access to everything. No user routes available.                                                                                                                                                      |
| **RBAC**                 | `CYBORGDB_SERVICE_ROOT_KEY` set   | The root key has admin (mint/list/revoke users, create/train/delete indexes). Per-user `cdbk_…` keys are scoped to one index with `read` / `write` permissions. The legacy `CYBORGDB_API_KEY` is no longer accepted on index routes. |

You enable RBAC by setting **both** keys at startup:

```bash theme={null}
export CYBORGDB_API_KEY=cyborg_your_legacy_key      # still required (license)
export CYBORGDB_SERVICE_ROOT_KEY=cyborg_your_root_key   # opt-in: enables RBAC
cyborgdb-service
```

Or via YAML:

```yaml cyborgdb.yaml theme={null}
service:
  cyborgdb_api_key: ${CYBORGDB_API_KEY}
  cyborgdb_service_root_key: ${CYBORGDB_SERVICE_ROOT_KEY}
```

<Warning>Pick **fresh, distinct** values for the two keys. The root key has admin privileges; treat it like a master credential. Rotate it independently of `CYBORGDB_API_KEY`.</Warning>

## Key kinds

When RBAC is on, the service classifies every `X-API-Key` request header into one of three kinds:

| Kind     | Header value                                                | What it can do                                                                                                                                       |
| -------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `root`   | `CYBORGDB_SERVICE_ROOT_KEY`                                 | Full admin: create/train/delete indexes; mint/list/revoke users; all data operations.                                                                |
| `user`   | A `cdbk_…` token minted via `POST /v1/indexes/{name}/users` | Data operations on **one** index, gated by the `read` / `write` permission set requested at mint time. Cannot create/delete indexes or manage users. |
| `legacy` | `CYBORGDB_API_KEY`                                          | **Rejected on index routes** when RBAC is on. Health endpoint still works.                                                                           |

If `CYBORGDB_SERVICE_ROOT_KEY` is unset, the legacy single key keeps its full-access behavior — RBAC is strictly opt-in.

## Provisioning users

A user is **scoped to exactly one index**, with a non-empty subset of `{"read", "write"}` permissions. Mint a user with the root key:

<CodeGroup>
  ```bash cURL icon="rectangle-terminal" theme={null}
  curl -X POST "http://localhost:8000/v1/indexes/documents/users" \
       -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY" \
       -H "Content-Type: application/json" \
       -d '{
         "permissions": ["read", "write"]
       }'
  # Response (returned ONCE — capture it):
  # { "user_id": "ab12…", "api_key": "cdbk_…" }
  ```

  ```python Python SDK icon="python" theme={null}
  from cyborgdb import Client

  admin = Client(base_url='http://localhost:8000', api_key=ROOT_KEY)
  index = admin.load_index('documents')  # KMS-backed: no index_key

  result = index.create_user(permissions=['read', 'write'])
  user_id = result['user_id']
  api_key = result['api_key']
  # Hand `api_key` to the new user via a secure channel — it is never recoverable.
  # Avoid logging or printing it.
  ```

  ```typescript TypeScript SDK icon="code" theme={null}
  import { Client } from 'cyborgdb';

  const admin = new Client({ baseUrl: 'http://localhost:8000', apiKey: ROOT_KEY });
  const index = await admin.loadIndex({ indexName: 'documents' });

  const { userId, apiKey } = await index.createUser({ permissions: ['read', 'write'] });
  // capture apiKey once — never recoverable
  ```

  ```go Go SDK icon="golang" theme={null}
  admin, _ := cyborgdb.NewClient("http://localhost:8000", rootKey)
  index, _ := admin.LoadIndex(context.Background(), "documents", nil)

  user, _ := index.CreateUser(context.Background(), []string{"read", "write"})
  // Hand user.APIKey to the new user via a secure channel — it is never recoverable.
  // Avoid logging or printing it.
  _ = user.UserID
  _ = user.APIKey
  ```
</CodeGroup>

<Warning>**The `api_key` is returned exactly once and is never stored by the service.** If you lose it, you must revoke the user and mint a new one. Hand it to the user securely (out-of-band, encrypted channel, secret store).</Warning>

The user authenticates by passing `cdbk_…` as `api_key` to their `Client` — they need **no index key** of their own (the service resolves the index KEK server-side).

```python theme={null}
# End user
from cyborgdb import Client

client = Client(base_url='http://localhost:8000', api_key='cdbk_…')
index = client.load_index('documents')  # no index_key argument
index.query(query_vectors=[0.1]*384, top_k=5)
```

## Listing and revoking users

<CodeGroup>
  ```bash cURL icon="rectangle-terminal" theme={null}
  # List
  curl -X GET "http://localhost:8000/v1/indexes/documents/users" \
       -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY"

  # Revoke
  curl -X DELETE "http://localhost:8000/v1/indexes/documents/users/ab12…" \
       -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY"
  ```

  ```python Python SDK icon="python" theme={null}
  users = index.list_users()
  for u in users:
      print(u['user_id'], u['permissions'])

  index.delete_user('ab12…')  # cryptographic revocation
  ```

  ```typescript TypeScript SDK icon="code" theme={null}
  const users = await index.listUsers();
  await index.deleteUser({ userId: 'ab12…' });
  ```

  ```go Go SDK icon="golang" theme={null}
  users, _ := index.ListUsers(context.Background())
  _ = index.DeleteUser(context.Background(), "ab12…")
  ```
</CodeGroup>

Revocation is **cryptographic, not policy-based**: the service erases that user's wrapped DEK(s) for the index. On the next request, the service has nothing to unwrap for them — even a captured `cdbk_…` token becomes useless. There is no propagation lag.

## KMS-backed constraint

Per-user keys work **only against KMS-backed indexes** — i.e., indexes created with `kms_name=…` rather than `index_key=…`. The reason is mechanical: a user does not hold the index key, so the service must be able to resolve it server-side from a `kms.registry` slot. For SDK-supplied indexes (`provider: none`), there is no server-side key to resolve, so per-user provisioning is not supported.

If you want multi-tenant access to an index that today uses an SDK-supplied key, migrate it to KMS-backed first (see [Managing Encryption Keys → Migrating](./managing-keys#migrating-from-sdk-supplied-to-kms-backed)).

<Note>Mint-time provisioning still requires the index KEK to bootstrap the new user's wrapped DEK. For KMS-backed indexes this happens server-side; for SDK-supplied indexes you'd need to pass the index key on the `create_user` call, but the resulting user still wouldn't have a path to authenticate (the service can't resolve the KEK for their later requests). This is why the constraint exists end-to-end.</Note>

## End-to-end example

A typical operator workflow for onboarding a new tenant:

<Steps>
  <Step title="Provision a KMS slot">
    Add a registry entry to your service YAML — see [KMS & BYOK](./kms-byok).

    ```yaml cyborgdb.yaml theme={null}
    kms:
      registry:
        tenant-acme:
          provider: aws-kms
          key_id:   alias/cyborgdb-acme
          region:   us-east-1
    ```

    Restart the service. Look for `KMS registry loaded` in the logs.
  </Step>

  <Step title="Create the tenant's index (root)">
    ```bash theme={null}
    curl -X POST "http://localhost:8000/v1/indexes/create" \
         -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY" \
         -H "Content-Type: application/json" \
         -d '{
           "index_name": "acme-documents",
           "kms_name": "tenant-acme",
           "dimension": 384,
           "metric": "cosine"
         }'
    ```
  </Step>

  <Step title="Mint a user key for the tenant (root)">
    ```bash theme={null}
    curl -X POST "http://localhost:8000/v1/indexes/acme-documents/users" \
         -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY" \
         -H "Content-Type: application/json" \
         -d '{ "permissions": ["read", "write"] }'
    ```

    Hand the returned `cdbk_…` to the tenant out-of-band.
  </Step>

  <Step title="Tenant uses the index">
    The tenant configures their SDK client with `api_key = "cdbk_…"` and operates only on `acme-documents`. They cannot list, describe, or touch any other tenant's index.
  </Step>

  <Step title="Revoke when access ends">
    ```bash theme={null}
    curl -X DELETE "http://localhost:8000/v1/indexes/acme-documents/users/<user_id>" \
         -H "X-API-Key: $CYBORGDB_SERVICE_ROOT_KEY"
    ```

    Revocation is immediate — the tenant's next request fails with 401.
  </Step>
</Steps>

## Operational notes

* **Read vs write semantics.** `read` covers `query`, `get`, `list_ids`, `describe`. `write` covers `upsert`, `delete`. Index lifecycle operations (`create`, `train`, `delete_index`) and user management are root-only — there's no "delegated admin" permission.
* **KEK cache.** Plaintext index KEKs live in an in-process cache for `INDEX_KEK_CACHE_TTL_SECONDS` (default 60s). Revoking a user is immediate and cryptographic; revoking *KMS-level* access to the wrap key propagates within the cache TTL.
* **Restarting the service.** User keys survive restart — the wrapped DEKs are persisted alongside the index envelope. The root key is read from env/YAML on every start.
* **Auditing.** The service logs the kind of key (`root` / `user` / `legacy` / `none`) on each request at `INFO`. Pair with structured logging to attribute index activity to users.

## See also

* [Per-Index KMS & BYOK](./kms-byok) — required for per-user keys.
* [Environment Variables](./env-vars) — `CYBORGDB_SERVICE_ROOT_KEY`, `INDEX_KEK_CACHE_TTL_SECONDS`.
* REST API: [Create User](../../rest-api/users/create-user), [List Users](../../rest-api/users/list-users), [Delete User](../../rest-api/users/delete-user).
