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_ENFORCEDor 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.