Skip to content

[finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287

Description

@baozhoutao

Recording an observation for the platform to rule on.

Good news first: an API key does not leak across tenants. Every probe below stayed empty or 404 — there is no cross-organization exposure here.

The finding is the other side of that: on a deployment running OS_TENANCY_POSTURE=isolated, a minted key appears unable to read any organization's data, which makes the key surface effectively inert while the console still offers minting.

Measured

Minted as the owner of 测试组织甲, from a session whose active organization was that org:

POST /api/v1/keys {"name":"probe-key-orgA"}
→ 201 {"success":true,"data":{"id":"…","prefix":"osk_…","key":"osk_…"}}

Then, credentials: 'omit' so only the key authenticates:

request (header x-api-key)result
no auth at all (control)401 UNAUTHENTICATED
GET /data/sys_user200, total 1 — only the key's own owner
GET /data/sys_team200, total 0 (the org has 1 team)
GET /data/sys_team/<orgA team id>404
GET /data/sys_team/<orgB team id>404
GET /auth/me/permissions200 with an empty body — no tenantId, no positions

Attempts to give the key an organization, all ineffective:

  • minting with {"organizationId": "<orgA>", "organization_id": "<orgA>"} → key created, still sys_team total 0
  • request headers x-organization-id, x-org-id, organization-id → still total 0

Why it lands this way

sys_api_key has no organization column at all — its declared fields are name, prefix, user_id, scopes, expires_at, last_used_at, revoked, key, id, created_at, updated_at (packages/platform-objects/src/identity/sys-api-key.object.ts). Key auth therefore establishes a user but no active organization, and the isolated wall is organization_id = activeOrganizationId (packages/spec/src/security/tenancy-posture.ts:15) — with no active org, no row can match. sys_user is the exception only because it is walled by an enumerated member-id list rather than by the org column.

The decision

  1. Intended — keys are a user-level surface, org-scoped automation is meant to use something else. Then the console's API-keys entry should say so, because today a tenant admin can mint a key, get a valid-looking secret, and discover only at call time that it reads nothing.
  2. Gap — keys should carry an organization (minted against the active org, or with an explicit org + membership check at mint time), and key auth should establish that org as the request's active organization.

Option 2 is the one that makes the existing UI honest, but it is a real security-surface decision (a long-lived credential pinned to a tenant), so it should be made deliberately rather than by patching the symptom.

Environment

objectos-ee-deploy (Caddy → app → postgres:16, NODE_ENV=production, OS_TENANCY_POSTURE=isolated) on http://localhost:8080, observed 2026-08-13. Two probe keys were minted during this test and are still present on the owner account (probe-key-orgA, probe-key-bound) — they carry no org access, but delete them if the environment is kept.

Related: #8286 (the same isolation wall, described back to callers in analytics SQL).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions