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_user | 200, total 1 — only the key's own owner |
GET /data/sys_team | 200, 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/permissions | 200 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
- 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.
- 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).
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:
Then,
credentials: 'omit'so only the key authenticates:x-api-key)401 UNAUTHENTICATEDGET /data/sys_user200, total 1 — only the key's own ownerGET /data/sys_team200, total 0 (the org has 1 team)GET /data/sys_team/<orgA team id>404GET /data/sys_team/<orgB team id>404GET /auth/me/permissions200with an empty body — notenantId, nopositionsAttempts to give the key an organization, all ineffective:
{"organizationId": "<orgA>", "organization_id": "<orgA>"}→ key created, stillsys_teamtotal 0x-organization-id,x-org-id,organization-id→ still total 0Why it lands this way
sys_api_keyhas no organization column at all — its declared fields arename, 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 theisolatedwall isorganization_id = activeOrganizationId(packages/spec/src/security/tenancy-posture.ts:15) — with no active org, no row can match.sys_useris the exception only because it is walled by an enumerated member-id list rather than by the org column.The decision
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) onhttp://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).