Agent-Hellboy / Agent-Hellboy/mcp-runtime
Per-audience token isolation via gateway-side token exchange
- Lingua principale
- Go
- Stelle
- 6
- Fork
- 1
- Merge medio
- 11h 33m
- PR unite (30g)
- 13
Descrizione
## Summary
Today the platform issues a **single user access token whose `aud` covers all three services** (`[platform-api, runtime-api, analytics-api]`, 15m TTL — `services/platform-api/internal/platformstore/auth.go:355` → `pkg/platformauth/audience.go` `RequiredAudiences()`). The CLI/UI store that one bearer and send it to the gateway, which path-routes to whichever backend. Because the token is multi-audience, a token leaked from one service is replayable against the other two for its whole TTL.
Goal: **per-audience isolation** — a token captured from runtime-api must not work against platform-api.
Raised as a design item in the PR #324 review. The naive fix (per-service token map at login) is architecturally unsound: clients hit a single gateway endpoint and don't know which backend a `/api/v1/*` path routes to, so they can't pick the right token. Decision: **gateway-side token exchange**, clients unchanged.
## Design: gateway-side token exchange
The gateway is **Traefik** (`mcp-sentinel-gateway`), with an existing forward-auth precedent.
1. **New platform-api internal exchange endpoint** beside the existing `platforminternal` handlers (`services/platform-api/routes.go:19-22`, gated by `INTERNAL_AUTH_TOKEN`). Verifies the inbound user token (aud=`platform-api`) and mints a fresh short-lived token with the target audience. Mirror the registry-authz handler shape (`services/platform-api/registry/authz.go:35-52`).
2. **Traefik forward-auth on the runtime-api + analytics-api routes only** (currently no middlewares — `k8s/10-gateway.yaml:601-750`; runtime `/api/v1/runtime/...`→`mcp-runtime-api:8084`, analytics `/api/v1/user/analytics`→`mcp-analytics-api:8085`). Mirror the `registry-admin-auth` middleware (`config/ingress/base/dynamic-config.yaml:10-18`) / `sentinel-admin-auth` (`k8s/10-gateway.yaml:31-34`); use `authResponseHeaders: Authorization` so Traefik replaces the upstream header with the exchanged per-audience token.
3. **Narrow the issued login token to `aud=platform-api`** (`CreateAccessToken`, `auth.go:340-356`). `Sign`/`Verify` already take audience args (`pkg/platformauth/sign.go:12`, `verify.go:11`); each service already verifies its own audience (`platform` `auth.go:363`; runtime `main.go:75`; analytics `main.go:71`).
4. **Update expectations**: `docs/security/authz-matrix.json` + the three `authz_matrix_test.go`, route-ownership tests, and `test/e2e` request-flow scripts that assume one token works on all services.
## Direct-access analysis (from wiring map)
- ✅ **Service-to-service calls are SAFE/unaffected** — runtime→platform (`runtime-api/internal/platformclient/client.go`) and analytics→platform (`analytics-api` `identity.HTTPResolver`) use `INTERNAL_AUTH_TOKEN`, not the user JWT.
- ✅ **CLI is SAFE** — `internal/cli/platformapi/client.go` calls the gateway/ingress URL, so forward-auth exchange applies.
- ⚠️ **UI is the gating concern.** The UI logs in directly against platform-api in-cluster (`services/ui/main.go:115`, login/oidc — fine, public), then **stores the token in session and proxies subsequent user API calls with it** as `Bearer `. If a narrowed `aud=platform-api` token is what the UI replays to runtime-api/analytics-api, those calls 401 — **unless** the UI proxy routes through Traefik (where exchange runs). Must confirm the UI→runtime/analytics proxy path.
## Open questions (do first — gate feasibility)
- [ ] **Does the UI proxy reach runtime-api/analytics-api via Traefik (exchange applies) or via direct in-cluster DNS (exchange bypassed)?** This is the gating question.
- [ ] If the UI bypasses Traefik for backend calls, pick a resolution: **(A)** auth response returns both a narrowed `aud=platform-api` token and a proxy token; **(B)** route the UI's backend proxy calls through Traefik; or **(C)** the exchange endpoint accepts the platform token and returns a per-route narrowed token. (A) or (B) preferred.
- [ ] Confirm Traefik `forwardAuth` + `authResponseHeaders` can **replace** (not just append) the upstream `Authorization` header.
## Acceptance criteria
- [ ] A user token (aud=`platform-api`) is rejected by runtime-api/analytics-api when presented **directly**.
- [ ] Through the gateway, CLI + UI flows calling runtime-api/analytics-api keep working (token swapped transparently).
- [ ] Exchanged tokens are short-lived + single-audience; unit tests on the exchange handler + minting.
- [ ] Kind e2e (`test/e2e/kind.sh`) passes with forward-auth enforced on the split routes.
## Risk / rollout
- Touches the request path for **all authenticated control-plane traffic** — a misconfigured middleware breaks auth cluster-wide.
- Traefik forward-auth wiring can only be fully validated on a live Kind cluster (qa-cluster-bringup + qa-e2e), not unit tests alone.
- **Phased rollout:** ship exchange endpoint + per-audience minting (additive, non-breaking) → wire Traefik + resolve the UI path → validate on a cluster → narrow `CreateAccessToken` last.
## Context
Surfaced during the PR #324 review (split of `mcp-sentinel-api`). Related: `pkg/platformauth/{sign,verify,audience,middleware}.go`, `services/platform-api/internal/platforminternal/`, `services/platform-api/internal/platformstore/auth.go`, `services/ui/main.go`, `config/ingress/`, `k8s/10-gateway.yaml`.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Valutazione
Questa issue non è ancora stata valutata.