Sentinel API authn/authz matrix¶
This is the source of truth for which roles can call which endpoint on the
split Sentinel API services (mcp-platform-api, mcp-runtime-api,
mcp-analytics-api). The security-audit-platform skill (see
.codex/skills/security-audit-platform/SKILL.md, Step 2) compares the live
services against this table. A divergence in either direction is a finding:
- A route in
services/platform-api/routes.go,services/runtime-api/routes.go, orservices/analytics-api/routes.gothat is missing from this table → add a row, then verify the expected status. - A row whose expected status differs from the live response → fix the route or fix the matrix, but do not silently change one to match the other.
Source of routes: per-service routes.go files (public surface is /api/v1/*
only). Path items ending with / route to a handler that parses sub-paths
internally; include both the prefix and the meaningful sub-paths in the table.
Roles¶
| Role | Credential | Source secret key |
|---|---|---|
anon |
none | n/a |
user-cookie |
logged-in browser session (platform identity) | seeded via PLATFORM_DEV_* in test mode, OIDC otherwise |
user-key |
x-api-key matching a user-scoped key |
mcp-sentinel-secrets.UI_API_KEY (also dual-purposed for UI) |
admin-key |
x-api-key matching ADMIN_API_KEYS entry |
mcp-sentinel-secrets.ADMIN_API_KEYS |
ingest-key |
x-api-key matching INGEST_API_KEYS entry |
mcp-sentinel-secrets.INGEST_API_KEYS |
admin-role is enforced by platformauth.Authenticator.RequireRole wraps in
each split service's routes.go. user-cookie and user-key distinguish which
calls require browser identity vs accept a bearer-style key.
When ADMIN_API_KEYS is unset, static API_KEYS authenticate as user
unless the explicit legacy dev/test fallback is enabled.
Expected codes:
- 200/204 — allowed; handler returns successfully.
- 401 — auth missing/invalid.
- 403 — auth valid but role insufficient.
- 404 — handler returns not-found for valid auth (path-item endpoints).
- 405 — method not allowed (handler-side check).
Public endpoints¶
| Path | Methods | anon | user-cookie | user-key | admin-key | ingest-key | Notes |
|---|---|---|---|---|---|---|---|
/health |
GET | 200 | 200 | 200 | 200 | 200 | Returns 503 when runtimeInit != "". |
/api/v1/auth/login |
POST | 200 | 200 | 200 | 200 | 200 | Local password login when PLATFORM_DEV_* enabled. |
/api/v1/auth/oidc |
POST | 200 | 200 | 200 | 200 | 200 | OIDC code exchange. |
/api/v1/auth/signup |
POST | 200 | 200 | 200 | 200 | 200 | Disabled in prod when configured; verify before deploy. |
All four of these routes are reachable without auth by design. If any later mutates state on behalf of a caller without further checks, that is a Critical finding.
User-authenticated endpoints (any authenticated identity)¶
| Path | Methods | anon | user-cookie | user-key | admin-key | ingest-key | Notes |
|---|---|---|---|---|---|---|---|
/api/v1/auth/me |
GET | 401 | 200 | 200 | 200 | 401/403 | Returns identity claims; ingest-only keys are not API auth identities. |
/api/v1/user/registry-credentials |
GET, POST | 401 | 200 | 200 | 200 | 401/403 | Ingest-only key must NOT manage user creds. Verify rejection. |
/api/v1/user/registry-credentials/{id} |
GET, PUT, DEL | 401 | 200 | 200 | 200 | 401/403 | Same. |
/api/v1/user/activity/image-publish |
GET | 401 | 200 | 200 | 200 | 401/403 | User-scoped read; ingest key should not see other users. |
/api/v1/user/api-keys |
GET, POST | 401 | 200 | 200 | 200 | 401/403 | Lifecycle for user-owned keys. |
/api/v1/user/api-keys/{id} |
GET, DEL | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/runtime/servers |
GET, POST | 401 | 200 | 200 | 200 | 401/403 | List/create MCP servers. |
/api/v1/runtime/observability/links |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Normal users are limited to team namespaces or caller-owned catalog servers. |
/api/v1/runtime/observability/grafana/dashboard |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Renders a server-scoped dashboard through the API. |
/api/v1/runtime/observability/prometheus/query |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | PromQL is allowlisted and server-scoped by the API. |
/api/v1/runtime/teams |
GET | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/runtime/teams |
POST | 401 | 403 | 403 | 200 | 401/403 | Admin-only team + namespace provisioning. |
/api/v1/runtime/teams/{id} |
GET | 401 | 200 | 200 | 200 | 401/403 | Team members can read only their teams; admins can read all teams. |
/api/v1/runtime/teams/{id}/members |
GET, POST | 401 | 200 | 200 | 200 | 401/403 | POST requires admin or team owner. |
/api/v1/users |
POST | 401 | 403 | 403 | 200 | 401/403 | Admin-only password user create. |
/api/v1/runtime/namespaces |
GET | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/runtime/namespaces/{name} |
GET | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/deployments |
GET | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/deployments/{id} |
GET | 401 | 200 | 200 | 200 | 401/403 | |
/api/v1/runtime/server-events |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Full event details only for admin, server owner, or team owner; regular namespace readers are forbidden. |
/api/v1/runtime/grants |
GET | 401 | 200 | 200 | 200 | 401/403 | Lists only grants for servers the caller can administer; regular team/catalog readers receive an empty scoped set. |
/api/v1/runtime/grants |
POST | 401 | 200/403 | 200/403 | 200 | 401/403 | Create/update requires admin, server owner, or team owner. |
/api/v1/runtime/grants/{ns}/{name} |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Full grant summary only for admin, server owner, or team owner. |
/api/v1/runtime/grants/{ns}/{name} |
DELETE | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; same owner/team-owner gate as apply. |
/api/v1/runtime/grants/{ns}/{name}/enable |
POST | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; same owner/team-owner gate as apply. |
/api/v1/runtime/grants/{ns}/{name}/disable |
POST | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; same owner/team-owner gate as apply. |
/api/v1/runtime/sessions |
GET | 401 | 200 | 200 | 200 | 401/403 | Lists only sessions for servers the caller can administer. |
/api/v1/runtime/sessions |
POST | 401 | 403 | 403 | 200 | 401/403 | Direct session apply is admin/internal-only; users should use /api/v1/runtime/adapter/sessions. |
/api/v1/runtime/sessions/{ns}/{name} |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Full session summary only for admin, server owner, or team owner. |
/api/v1/runtime/sessions/{ns}/{name} |
DELETE | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; requires admin, server owner, or team owner. |
/api/v1/runtime/sessions/{ns}/{name}/revoke |
POST | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; requires admin, server owner, or team owner. |
/api/v1/runtime/sessions/{ns}/{name}/unrevoke |
POST | 401 | 200/403 | 200/403 | 200 | 401/403 | Mutating; requires admin, server owner, or team owner. |
/api/v1/runtime/policy |
GET | 401 | 200/403 | 200/403 | 200 | 401/403 | Rendered policy is visible only to admin, server owner, or team owner. |
Admin-only endpoints (requireRole(roleAdmin, …))¶
| Path | Methods | anon | user-cookie (non-admin) | user-key | admin-key | ingest-key | Notes |
|---|---|---|---|---|---|---|---|
/api/v1/events |
GET | 401 | 403 | 403 | 200 | 403 | Admin event log. |
/api/v1/stats |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/sources |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/event-types |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/analytics/usage |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/dashboard/summary |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/admin/namespaces |
GET, POST | 401 | 403 | 403 | 200 | 403 | |
/api/v1/admin/audit |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/admin/operations |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/admin/deployments |
GET | 401 | 403 | 403 | 200 | 403 | |
/api/v1/runtime/components |
GET | 401 | 403 | 403 | 200 | 403 | Cluster component/workload health and errors. |
/api/v1/runtime/actions/restart |
POST | 401 | 403 | 403 | 200 | 403 | Cluster-affecting. |
Method gating¶
http.NewServeMux does not enforce HTTP methods; the handler does. For each
row above, the auditor must also confirm:
- The handler rejects methods not listed with 405.
- A method-confused request (e.g.,
OPTIONSfollowed byPOST) cannot bypass the role check. - Unknown sub-paths under
/api/v1/runtime/grants/and similar prefix routes return 404, not 200 with a default action.
Drift check¶
The platform audit harness in
.codex/skills/security-audit-platform/SKILL.md (Step 2) reads this table
and exercises each row. The starter harness lives in
docs/security/authz-matrix.json (subset of rows); unit tests in each split
service load that file via pkg/authzmatrix. Expand the JSON toward full table
parity over time. Each row object looks like:
{ "path": "/api/v1/events", "method": "GET", "role": "ingest", "expect": 403 }
Keep the JSON and the markdown in sync; the markdown is canonical for review, the JSON is canonical for the harness.
Open follow-ups¶
- Expand
docs/security/authz-matrix.jsontoward full parity with the markdown table; runbash hack/validate-authz-matrix.shon a live cluster for drift. - Confirm whether
user-keyanduser-cookieshould be merged for authorization purposes, or whether some routes (e.g., billing, admin bootstrap) intentionally accept only one credential type. - Document whether
/api/v1/runtime/*mutations are scoped to the calling user's tenancy, and how that tenancy is derived (subject claim, namespace label, etc.). The audit cannot judge cross-tenant safety without that anchor.