Skip to content
Security & Tenancy
Admin & Reference
On this page

Security & tenancy

How KeyOne keeps one customer’s data away from another’s, and what it does not do — stated because a security document that overstates its coverage is worse than none.

One binary, one tenant

A running KeyOne binary serves exactly one tenant. There is no org switcher, no “my organisations” list, and no way to be in two at once. The tenant is fixed by keyone.yaml at boot and resolved once, not per request.

Isolation is enforced by the tenant predicate in every query

Every tenant-scoped table has a row-level-security policy, enabled and forced (FORCE ROW LEVEL SECURITY — 69 tables), of the form:

USING      (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid)

Every read and write goes through store.WithTenant, which sets app.tenant_id for the duration of one transaction and scopes the statement by tenant_id.

Those policies are currently inert, and this section used to say otherwise. Measured against the live database, the role the application connects as has bypassrls set, and Postgres does not apply row-level security to a role that bypasses it. So all 44 policies are present, correct, forced — and enforcing nothing. The tenant_id predicate written into each query is what separates tenants today.

This is stated plainly because the opposite belief has already caused defects: two cross-tenant bugs were written by authors who read a comment saying RLS would catch a missing predicate, and so did not write one. Treating RLS as a backstop is what made those bugs possible. See core/internal/store/rlsnote.go for the measurement and the full account.

Making the policies effective is a one-line privilege change on the production database — granting the application a role without bypassrls. Nothing in the codebase needs to change: the policies are already written and already correct. Until that change is made, this page will not describe them as protection.

Who can do what

Roles are rep, analyst, admin, internal, carried in a signed JWT and checked by the router before any handler runs. The gates:

  • Analyst surface — hubs, decisions, queries, campaigns, reports, KeyChat, entity search, drill-down. Granted to analyst, and additively to admin and internal.
  • Admin surface — users, sources, engines, definitions, the reporting hierarchy, form-type authoring, and the whole ops section (run history, freshness, health, audit log, metrics). Admin and internal only, never analyst.
  • KeyLink — the field app runs its own chain, and enforces one rule structurally: seeing is not filing. An admin viewing as a rep may read everything and file nothing.

Personal surfaces are scoped to the caller’s own user id taken from the verified token — the notification inbox and KeyChat conversations have no route by which one user reaches another’s, and no admin override.

Sessions

Access tokens are short-lived and signed with a key that persists in the database, so a restart does not log everyone out and every instance behind a load balancer verifies the same tokens. Refresh tokens rotate on use, with a short grace window for concurrent refreshes; presenting a superseded token outside that window marks the whole token family compromised, and that verdict is never rolled back by a later valid refresh.

Credentials

Warehouse credentials never cross the API. The sources endpoint reports the connection’s non-secret configuration only — and specifically not the Postgres url, which carries a password inside it. The deployed keyone.yaml contains no secrets either: every credential is a ${VAR} expanded at boot, so the image is safe to push to a registry.

What KeyOne does NOT have

Named explicitly, because their absence used to be documented as presence:

  • No sub-tenant scope. There is no mechanism scoping writes to a region, a category, or a subset of stores. A user with a role has that role across the whole tenant. Field reps’ work feed is filtered to their assigned outlets, but that is a feed filter, not a write guard.
  • No dormant RLS flag. There is no KEYONE_SCOPE_RLS_ENFORCED or any environment switch that hardens isolation. What is described above is always on; there is no stronger mode to turn on.
  • No embed tokens and no policy-claim scoping of the query layer.

If any of those are needed, they are work to be done — not settings to be found.