# Tenant isolation (/security/tenant-isolation)



No reusable state is shared across tenants by default. Isolation is enforced at the data layer, not bolted on in application code, and it is verified destructively rather than assumed.

## Namespacing every object [#namespacing-every-object]

Every reusable object is namespaced by tenant ID at the database layer: artifacts, bundles, sessions, branches, snapshots, materializations, KV realizations, traces, replay runs, purge jobs, alias policies, and usage events. Application-layer filtering is never the only line of defense, because a single missing `WHERE project_id =` should not be able to leak another tenant's data.

Every authenticated request resolves to one project. The handlers operate inside that boundary, and reads are filtered by the caller's project before any object is returned. A request for an object that exists but belongs to another tenant returns a not-found, the same response as an object that does not exist, so existence is not leaked.

## Opaque public IDs [#opaque-public-ids]

Customer-facing handles are random opaque identifiers (`art_…`, `bnd_…`, `ses_…`, `br_…`, `snp_…`, `rsp_…`), never raw content hashes.

Raw content hashes would be dangerous as public handles: customers might treat them as durable capabilities, accidental cross-tenant correlation would get easier, re-uploaded content could resurrect old cache relationships, and a leaked hash could reveal that two systems hold equal content. Opaque IDs carry none of that signal. See [handles and fingerprints](/concepts/handles-and-fingerprints) for the full identity model.

## HMAC fingerprints stay internal [#hmac-fingerprints-stay-internal]

Deduplication and trace analysis need to recognize equal content, but that recognition must never escape the tenant boundary. Zumik computes a tenant-scoped keyed fingerprint:

```
logical_fingerprint = HMAC-SHA256(
  tenant_fingerprint_key,
  canonicalization_version || artifact_type || normalized_content
)
```

Because the key is per-tenant, two tenants holding identical content produce different fingerprints, so the fingerprint cannot be used as a cross-tenant equality oracle. The fingerprint is used only inside the isolation boundary. Rotating a tenant's fingerprint key intentionally breaks equality linkage, which is part of the resurrection-prevention story for [purge](/concepts/retention-and-purge).

Sealed secrets reinforce this: the same plaintext sealed twice produces different ciphertext (random nonce per seal), so ciphertext can never be used as an equality oracle either.

## Tested, not assumed [#tested-not-assumed]

Cross-tenant isolation is verified with destructive tests that run in CI and attempt to access one tenant's data using another tenant's credentials. The scope explicitly covers the access-control failure modes:

* **BOLA (Broken Object Level Authorization).** Verify that one tenant cannot read or mutate another tenant's artifacts, sessions, branches, API keys, billing records, or purge receipts by supplying their object IDs.
* **IDOR (Insecure Direct Object Reference).** Manipulating an ID in a path or body must never reach an object outside the caller's project.
* **Shared-cache bleed.** Shared cache paths are tested for cross-tenant KV bleed, since a physical cache hit must respect the [KV compatibility key](/concepts/handles-and-fingerprints), which includes the isolation namespace.

## Authorization on every route [#authorization-on-every-route]

Every API endpoint enforces authorization; no endpoint is reachable by a caller lacking the required project or tenant permission. Platform-level mutations such as managing model aliases require an `admin`-scoped key, so an inference-only key cannot reshape routing for the whole project. See [authentication](/authentication) for scopes.

<Card title="The full vulnerability scope" icon="list-check" href="/security/vulnerability-checklist">
  Multi-tenant data leakage and broken access control are tracked alongside the rest of the checklist.
</Card>
