109 lines
5.0 KiB
Markdown
109 lines
5.0 KiB
Markdown
# ADR-001: Admin endpoints accept any workspace bearer token
|
|
|
|
**Status:** Accepted — known risk, Phase-H remediation planned
|
|
**Date:** 2026-04-17
|
|
**Issue:** #684
|
|
**Tracking:** Phase-H — #710
|
|
|
|
## Context
|
|
|
|
The `AdminAuth` middleware validates callers by calling `ValidateAnyToken`, which
|
|
accepts any live workspace bearer token regardless of which workspace issued it.
|
|
There is no separation between workspace-scoped tokens (issued to individual
|
|
agents) and admin-scoped tokens (intended for platform operators).
|
|
|
|
This means any workspace agent that has been issued a token can reach every
|
|
admin-gated route on the platform.
|
|
|
|
## Decision
|
|
|
|
Proper token-tier separation (workspace vs. admin scope) is deferred to Phase-H.
|
|
The known risk is explicitly accepted. Mitigation controls are documented below.
|
|
|
|
## Blast radius — affected admin endpoints
|
|
|
|
A compromised workspace token grants unauthenticated-equivalent access to all
|
|
of the following:
|
|
|
|
| Endpoint | Impact |
|
|
|----------|--------|
|
|
| `GET /admin/workspaces/:id/test-token` | Mint a fresh bearer token for any workspace |
|
|
| `DELETE /workspaces/:id` | Delete any workspace and auto-revoke its tokens |
|
|
| `PUT /settings/secrets` / `POST /admin/secrets` | Overwrite any global secret (env-poisons every agent on restart) |
|
|
| `DELETE /settings/secrets/:key` / `DELETE /admin/secrets/:key` | Delete any global secret; same fan-out restart |
|
|
| `GET /settings/secrets` / `GET /admin/secrets` | Read all global secret keys (values masked, but key enumeration enables targeted attacks) |
|
|
| `GET /workspaces/:id/budget` + `PATCH /workspaces/:id/budget` | Read or clear any workspace's token budget |
|
|
| `GET /events` / `GET /events/:workspaceId` | Read the full structural event log across all workspaces |
|
|
| `POST /bundles/import` | Import an arbitrary workspace bundle — creates workspaces, injects secrets, overwrites configs |
|
|
| `GET /bundles/export/:id` | Exfiltrate full workspace bundle including config, secrets references, and files |
|
|
| `POST /org/import` | Instantiate an entire org template — creates multiple workspaces with arbitrary roles and secrets |
|
|
| `GET /org/templates` | Enumerate all org template names and their configured roles/system prompts |
|
|
| `POST /templates/import` | Write arbitrary files into `configsDir` (workspace template injection) |
|
|
| `GET /templates` | Enumerate all template names and metadata |
|
|
| `GET /admin/liveness` | Read platform subsystem health (ops intel) |
|
|
| `GET /admin/schedules/health` | Read cron scheduler health across all workspaces |
|
|
|
|
## Risk statement
|
|
|
|
**A single compromised workspace agent can achieve full platform takeover via
|
|
admin endpoints.**
|
|
|
|
Attack chain example:
|
|
1. Agent A's token is exfiltrated (e.g. via a prompt-injection in a delegated task).
|
|
2. Attacker calls `PUT /settings/secrets` to overwrite `CLAUDE_API_KEY` with a
|
|
controlled value.
|
|
3. Every non-paused workspace restarts and loads the poisoned key.
|
|
4. Attacker now controls the LLM backend for the entire platform.
|
|
|
|
Alternatively: call `POST /bundles/import` with a crafted bundle to inject a
|
|
malicious workspace with a pre-configured `initial_prompt` and elevated secrets.
|
|
|
|
## Current mitigations
|
|
|
|
- **Workspace isolation** — `CanCommunicate()` in the A2A proxy limits which
|
|
workspaces can send tasks to which, reducing the blast radius of a single
|
|
compromised agent during normal operation.
|
|
- **Audit logging** — PR #651 writes all admin-route calls to `structure_events`.
|
|
Forensic recovery is possible after the fact.
|
|
- **`ValidateAnyToken` removed-workspace JOIN** — tokens belonging to deleted
|
|
workspaces are filtered at the DB layer (PR #682 defense-in-depth) so
|
|
post-deletion token replay is blocked.
|
|
- **`MOLECULE_ENV=production` gate** — hides the `/admin/workspaces/:id/test-token`
|
|
endpoint in production deployments unless `MOLECULE_ENABLE_TEST_TOKENS=1`.
|
|
|
|
## Phase-H remediation plan
|
|
|
|
Tracked in GitHub issue **#710**.
|
|
|
|
### Schema change
|
|
|
|
Add a `token_type` column to `workspace_auth_tokens`:
|
|
|
|
```sql
|
|
ALTER TABLE workspace_auth_tokens
|
|
ADD COLUMN IF NOT EXISTS token_type TEXT NOT NULL DEFAULT 'workspace'
|
|
CHECK (token_type IN ('workspace', 'admin'));
|
|
```
|
|
|
|
Admin tokens are minted only via a dedicated privileged endpoint that itself
|
|
requires an existing admin token or a one-time bootstrap secret.
|
|
|
|
### Middleware update
|
|
|
|
- `WorkspaceAuth` — continue accepting `token_type = 'workspace'` only.
|
|
- `AdminAuth` — require `token_type = 'admin'`. Workspace tokens rejected.
|
|
|
|
### Bootstrap flow
|
|
|
|
On first boot (no tokens exist), a single-use bootstrap secret is printed to
|
|
the server log. The operator uses it to mint the first admin token. Subsequent
|
|
admin tokens are minted by existing admin token holders. The fail-open path in
|
|
`HasAnyLiveTokenGlobal` is retired once Phase-H ships.
|
|
|
|
### Migration path
|
|
|
|
Phase-H is a breaking change for any automation that currently uses workspace
|
|
tokens against admin endpoints. A migration guide and a `MOLECULE_PHASE_H=1`
|
|
feature flag will be provided so operators can opt in before the strict
|
|
enforcement date.
|