Tenants¶
Enterprise, from enterprise 0.33.0. Off unless TDB_TENANTS_DIR is set.
One TDB deployment can serve several teams or customers, each as a tenant that sees only
its own sources, keys, views, policies, approvals and audit log. Two tenants can both have a
source called sales and never meet: neither can name, query or audit the other's.
Each tenant's state lives in its own directory, with its own registry and its own hash-chained audit log. A request's tenant comes only from its credential, never from a header or a URL it could choose.
How it is laid out¶
TDB_TENANTS_DIR/
├── acme/
│ ├── tenant.yaml # id: '1a2b3c4d' (suspended: true, to suspend)
│ ├── registry.db # its sources, keys, annotations, approvals
│ ├── audit.jsonl # its own hash chain, with sealed segments beside it
│ ├── data/ # where its CSV, Parquet and DuckDB files go
│ ├── policies.yml # optional: its column and row policies
│ └── views/ # optional: its named views
├── beta/ …
└── .deployment/
└── audit.jsonl # the deployment's own chain (below)
default is the deployment as it was before tenants. Its state stays where the TDB_*
variables put it (TDB_REGISTRY_DB, TDB_LOG_FILE, TDB_ALLOWED_DATA_DIR,
TDB_POLICIES_FILE, TDB_VIEWS_DIR). Turning tenants on moves nothing.
What belongs to the deployment, not to a tenant: the people who sign in through your identity provider, SCIM, the OAuth sign-in state, the encryption key for stored credentials, the licence, the PostgreSQL wire gateway's port, and the webhook and SIEM destinations.
Who acts in which tenant¶
| Credential | Acts in |
|---|---|
A database key minted in a tenant: tdbt_<tenant id>_… |
That tenant |
A database key from before tenants: tdbk_… |
default |
| A person signed in by single sign-on | The tenant their groups name (below) |
A static TDB_API_KEYS key, or the TDB_ADMIN_USER login |
No tenant: the operator (below) |
A key edited to name another tenant is refused with 401: the tenant it names must hold it.
Static keys read no tenant, default included
With tenants on, TDB_API_KEYS keys are the operator's. Scripts that query with one stop
working. Before you set TDB_TENANTS_DIR, mint a database key for default and move
them to it: POST /v1/auth/keys today, or POST /v1/tenants/default/keys after. TDB logs
tenant_default_has_no_key at startup while default has none.
Turning tenants on¶
- Have an Enterprise licence. With any lower tier, TDB refuses to start while
TDB_TENANTS_DIRis set (Tenants are an Enterprise feature). - Mint a key for
default(see the warning above). - Give
defaulta data directory outsideTDB_TENANTS_DIR. TDB refuses to start ifTDB_ALLOWED_DATA_DIRis inside it. - If you use single sign-on, add
tenant:to every group inTDB_SSO_FILE(below). - Create an empty directory, set
TDB_TENANTS_DIRto it, and restart.
Creating a tenant¶
Run the CLI inside the container, then restart TDB:
docker exec tdb python -m tdb.cli tenants init acme
# Created tenant acme (id 1a2b3c4d).
# Its admin key, shown once; store it now:
# tdbt_1a2b3c4d_…
# Restart TDB to serve the new tenant.
docker restart tdb
init creates the directory with a fresh id and one admin key for the tenant, shown once.
That admin then works as admins do today, inside the tenant: registers sources, mints keys,
writes its policy file and views.
A tenant name is 1–32 characters of a-z, 0-9 and -, starting with a letter or digit.
Never create a tenant by copying another's directory: TDB refuses to start when two tenants
share an id or a source id.
Suspending a tenant¶
docker exec tdb python -m tdb.cli tenants suspend acme # then restart
docker exec tdb python -m tdb.cli tenants resume acme # then restart
A suspended tenant's keys and people are refused with 403. Its files stay where they are: TDB never deletes a tenant, its keys or its audit log.
The operator¶
With tenants on, the static TDB_API_KEYS keys and the TDB_ADMIN_USER login manage
tenants and read none. Everything the operator does that touches a tenant is on record.
| Request | What it does |
|---|---|
GET /v1/tenants |
Lists the tenants: name, id, suspended |
POST /v1/tenants/{name}/keys |
Mints a key inside a tenant (role, default read), recorded in that tenant's chain and in the deployment's |
GET /v1/audit/verify?tenant=acme |
Verifies acme's chain; ?tenant=_deployment the deployment's own |
GET /v1/audit/history?tenant=…, POST /v1/audit/rotate?tenant=… |
The same for history and rotation |
POST /v1/audit/export?tenant=…, GET /v1/audit/export/status?tenant=… |
Exports one tenant's chain to Splunk or S3 |
The operator must name the chain; without ?tenant= these return 400. A tenant's admin
calls them without ?tenant= and acts on its own chain; naming another is a 403.
tdb tenants list lists tenants through the server, with the operator's key in
TDB_API_KEYS.
Single sign-on¶
People belong to the deployment; their groups place them in a tenant. With tenants on,
every group in TDB_SSO_FILE names one:
groups:
acme-analysts: {role: read, tenant: acme}
acme-admins: {role: admin, tenant: acme}
beta-analysts: {role: read, tenant: beta}
TDB reads a person's groups on every request, so a change at sign-in or by SCIM moves them on
their next one, with the same token. Someone whose groups name two tenants is refused
(sso_tenant_ambiguous), and so is someone whose groups name none (sso_no_tenant). TDB
refuses to start if a group names no tenant, or one that is not in TDB_TENANTS_DIR.
See Single sign-on and SCIM for the rest of the group file.
The audit logs¶
Each tenant has its own chain, verified on its own, and every entry names it ("tenant":
"acme"). A tenant's admin verifies, rotates and exports its own chain exactly as before.
The deployment's chain, TDB_TENANTS_DIR/.deployment/audit.jsonl, records what belongs to
no tenant:
| Entry | When |
|---|---|
denied, action: tenant |
A request refused before it had a tenant: invalid_api_key (a key naming no tenant that holds it), tenant_suspended, sso_tenant_ambiguous, sso_no_tenant, sso_user_inactive |
denied, operator_names_no_tenant |
The operator called a tenant route |
operator_key_minted, operator_audit_rotated, operator_audit_export |
The operator acted on a tenant |
tenants_loaded |
Each start, with the tenants it serves, so creating or suspending one is on record |
A key forged to name a real tenant is refused by that tenant, so the attempt is in that tenant's chain.
S3 export: with tenants on, each chain's objects go under <prefix>/<tenant>/
(<prefix>/_deployment/ for the deployment's), because every chain numbers its entries from 1.
Files¶
A tenant's file sources must be inside its own data/ directory. A file under
TDB_TENANTS_DIR can be read only by the tenant whose directory holds it, whatever path or
symlink points at it, default included.
Database sources are not separated by TDB. Each tenant registers its own sources with its own database credentials. Give each tenant a database user that can read only that tenant's data: TDB keeps tenants from naming each other's sources, but a tenant that is given another tenant's database password can register it.
The PostgreSQL wire gateway¶
One port serves every tenant. A session's tenant comes from its password, the same as REST:
a tenant key, or a signed-in person's /sso/token. The operator cannot log in. TDB checks
before every statement that the credential still acts in the session's tenant, so a person
moved to another tenant loses the session.
Limits¶
- Creating, suspending and resuming a tenant take effect on restart.
- One identity provider for the deployment; each group places people in one tenant.
/metricsis the deployment's: it is not split by tenant.- Deleting a tenant is done by hand, by moving its directory away while TDB is stopped. TDB never deletes an audit log.