feat(hub): delegated tokens for platform providers behind a flag - #653
Merged
Merged
Conversation
mjudeikis
force-pushed
the
sec/1.2-delegated-tokens-for-org-providers
branch
from
September 5, 2026 15:31
a2d6321 to
56decbf
Compare
mjudeikis
force-pushed
the
sec/3.3-delegated-tokens-for-platform-providers
branch
from
September 5, 2026 15:31
9185098 to
5bd477d
Compare
mjudeikis
force-pushed
the
sec/1.2-delegated-tokens-for-org-providers
branch
from
September 6, 2026 09:05
56decbf to
a960edd
Compare
mjudeikis
force-pushed
the
sec/3.3-delegated-tokens-for-platform-providers
branch
from
September 6, 2026 09:07
5bd477d to
0795a5d
Compare
mjudeikis
force-pushed
the
sec/1.2-delegated-tokens-for-org-providers
branch
from
September 6, 2026 09:43
a960edd to
ed3d19d
Compare
mjudeikis
force-pushed
the
sec/3.3-delegated-tokens-for-platform-providers
branch
from
September 6, 2026 10:08
0795a5d to
f603781
Compare
mjudeikis
changed the base branch from
sec/1.2-delegated-tokens-for-org-providers
to
main
September 6, 2026 11:48
mjudeikis
force-pushed
the
sec/3.3-delegated-tokens-for-platform-providers
branch
from
September 6, 2026 11:49
f603781 to
1f0b8a4
Compare
Step C of remediation item 1.2, plan item 3.3. Step A stopped forwarding
the caller's hub bearer to org-owned providers; platform providers still
received it. That bearer reaches every workspace the user belongs to and
every hub REST endpoint they can call, so a bug in any first-party
provider acts as the user everywhere, not in the one workspace the
request was made in.
Platform providers can now receive the same delegated ServiceAccount
token — minted in the caller's current workspace, ten minutes, bound to
the role they hold there — behind --provider-delegated-tokens=off|
platform|all. Default off this release; the doc comment and the flag help
say the next release defaults to platform, so deployments can verify
first. Fail-closed semantics match step A exactly: unresolved caller or no
workspace selection is 403, missing issuer or mint failure is 503, and no
path falls back to forwarding the bearer. Anonymous health probes still
carry no credential.
Every consumer of the forwarded bearer was audited (verdict table in
docs/provider-scoping.md). No provider parses the caller's token as a
JWT; identity comes from X-Faros-User, which is unchanged, and the bearer
is only ever an opaque credential for {hub}/clusters/{id}, /graphql/
{cluster}, or an SSAR. The one genuine break is the edges SSH data plane
with spec.sshUserMapping=identity, which turns the TokenReview'd username
into the Linux login name; edges is excluded by name via
--provider-delegated-tokens-exclude rather than left half-working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mjudeikis
force-pushed
the
sec/3.3-delegated-tokens-for-platform-providers
branch
from
September 6, 2026 13:25
1f0b8a4 to
d581ea6
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The change is well-contained behind a default-off flag with solid tests and fail-closed behavior; only a minor test assertion should be cleaned up.
Pull request overview
This PR adds an opt-in (flagged) mechanism for the hub backend proxy to swap a caller’s full hub bearer token for a short-lived, workspace-scoped delegated ServiceAccount token when proxying to platform providers, reducing blast radius while keeping the current default behavior unchanged.
Changes:
- Introduces
--provider-delegated-tokens=off|platform|alland--provider-delegated-tokens-excludewiring (defaults tooff, with default exclusion foredges). - Extends the backend provider proxy to apply delegated-token substitution to platform providers when the delegation policy selects them (fail-closed; UI proxy unaffected).
- Adds targeted tests for delegation policy behavior and delegated-token handling in the edges tunnel auth path, plus documentation updates describing the model and rollout.
File summaries
| File | Description |
|---|---|
| providers/edges/internal/tunnel/auth_test.go | Adds tests covering how delegated SA tokens are authenticated/authorized by edges tunnel auth. |
| pkg/hub/server.go | Parses delegation mode early and wires the delegation policy into the backend proxy at startup. |
| pkg/hub/providers/proxy.go | Applies delegation policy to platform-provider backend proxying (swap Authorization when selected). |
| pkg/hub/providers/proxy_edge.go | Generalizes delegated-token minting logic for both org-owned providers and delegated platform providers. |
| pkg/hub/providers/proxy_edge_test.go | Updates comment to reflect that platform delegation is now behind an opt-in flag. |
| pkg/hub/providers/proxy_delegation.go | Adds delegation mode/policy types, parsing, and default exclusion list. |
| pkg/hub/providers/proxy_delegation_test.go | Adds comprehensive tests for delegation modes, exclusions, fail-closed behavior, and UI proxy non-interference. |
| pkg/hub/options.go | Adds new Options fields and defaults for delegated-token behavior and exclusions. |
| docs/provider-scoping.md | Documents delegated-token behavior, modes, failure semantics, and per-provider audit notes. |
| docs/hub-proxy-workspace-access.md | Documents delegated tokens in the hub proxy access model and references provider-scoping details. |
| cmd/faros-hub/main.go | Exposes the new delegated-token flags and help text in the hub CLI. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+179
to
+183
| // Both reviews go to the tenant config (the APIExport VW for the addressed | ||
| // cluster), never to the provider's own workspace. | ||
| if strings.Contains(rec.tokenReviewPaths[0], "provider-workspace") { | ||
| t.Errorf("TokenReview path %q, want the tenant VW", rec.tokenReviewPaths[0]) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #635 — must merge after it. Base is
sec/1.2-delegated-tokens-for-org-providers; the diff shown here is only step C on top of it.Security remediation plan item 3.3 (step C of item 1.2, finding 2).
Problem
#635 stopped forwarding the caller's hub bearer to org-owned providers, and pinned platform providers as unchanged (
TestBackendProxyPlatformProviderStillReceivesCallerBearer). Sopkg/hub/providers/proxy.gostill hands a platform provider the caller's ownAuthorizationheader.That header is a full hub credential. Under A-1 (
docs/hub-proxy-workspace-access.md) it reaches every workspace the user is a member of, plus every hub REST endpoint they can call. A platform provider needs far less — it acts in one workspace, on the caller's behalf. The gap is not hypothetical retention-wise:providers/agentsholds the caller's token for the lifetime of a detached run (api/run.goHubToken, resumed oncontext.WithoutCancel),providers/app-studioholds it for a sandbox's lifetime (assistant_run_sandbox_core.goid identity) and re-forwards it verbatim to the hub MCP aggregate and to other providers' action routes (api/llm.go,api/integrations.go), andproviders/databrickscaches clients built from it for 10 minutes (tenant/client.go). A bug or compromise in any of them acts as the user everywhere, not in the one workspace the request was made in.Change
Flag.
--provider-delegated-tokens=off|platform|allplus--provider-delegated-tokens-exclude(pkg/hub/options.go,cmd/faros-hub/main.go).offis today's behaviour and this release's default; the doc comment and flag help both say the next release defaults toplatform. Parsed once at startup before any expensive init, so a typo fails in milliseconds rather than after kcp boots.Policy type.
pkg/hub/providers/proxy_delegation.go—DelegationMode,ParseDelegationMode,DelegationPolicy.DelegatesPlatform(name), andDefaultDelegationExclude. Kept separate from the proxy so the audit verdict behind the exclusion list lives next to the list itself. Zero value isoff, so a proxy built withoutSetDelegationPolicy(every test, and any embedder) behaves exactly as before.Proxy. The platform branch of
ServeHTTPreuses step A'sdelegatedAuthorizationbefore building the reverse proxy, and the Director deletesAuthorizationaftersetHeadersand sets the delegated bearer — the same two lines, in the same order, asserveOverEdge.delegatedAuthorizationwas generalised: the org-ownership check now only applies whenprov.OrgUUID != ""(a platform provider has no owning org, so the token is minted in the caller's org), and a caller with no resolvable org is refused. Everything else is unchanged, including the 403/503 split. The UI proxy is excluded structurally (!p.fallbackForSPA), so asset loads cannot start 403ing when the flag is turned on.Workspace scope. The delegated account is minted in the workspace the request targets (
X-Faros-Workspace, verified against the caller's membership by the resolver before it becomesX-Faros-Tenant). Org-scope requests with no workspace header are refused with 403, as in step A. I checked whether platform providers legitimately receive org-scope requests today and concluded they do not:portalkit'stenantHeaders()sends both headers,stores/tenant.tsnormalises organization-only mode toworkspaceUUID = null, andProviderFrame.vuegatesaccessAllowedon a workspace being selected. Minting in the org workspace is not an option regardless — O-10 seals org workspaces and the hub's SA proxy path (serveServiceAccount) refuses a token whose cluster claim is one.Nothing under
providers/orprovider-sdk/changed except a new test file.Compatibility
Flag default is
off— no deployment changes behaviour on upgrade. Org-owned providers are unaffected either way: they always get the delegated token, as of #635.Rollout order. Merge #635 → ship this with
off→ runplatformin staging and confirm no provider 403s → flip production toplatform→ next release changes the default, at which pointoffremains available as an escape hatch.What each provider needs. Per-consumer audit; the full table with file references is in
docs/provider-scoping.md. The recurring answer is "nothing", because no provider parses the caller's bearer as a JWT (a repo-wide search forParseUnverified/jwt.Parse/ParseSignedunderproviders/andprovider-sdk/finds only tokens that are not the caller's) and every provider takes identity fromX-Faros-User, which the hub still sends unchanged.provider-sdk/tenantaccessNewClient{hub}/clusters/{id}clientproviders/infrastructure/dataplane/identity.go+authorizer.go<resource>/exec; caller-scoped instance GET as the access gateX-Faros-Useris carried but ignored by the authorizer.providers/infrastructure/tenant/,providers/code/tenant/{hub}/clusters/{X-Faros-Cluster}dynamic + authorization clients,BearerToken: tokenproviders/edges— tunnel, k8s subresource,services/{name}/proxyverb: proxy) via the APIExport VW for the addressed clusterproviders/edges— SSH,spec.sshUserMapping: identityfetchSSHCredentials)system:serviceaccount:default:faros-du-<hash>.edgesis inDefaultDelegationExcludewith a comment; fixing it means sourcing that identity fromX-Faros-User.providers/app-studio{hub}/clusters/{id}DELETE; re-forwards the bearer to the hub MCP aggregate, provider action routes, infra data planeX-Faros-*.providers/agents(tenant/graphql.go){hub}/graphql/{cluster}; forwards to edges MCP + infra data plane/s2sendpoint, which is not hub-proxied and is untouched.providers/databricks{hub}/clusters/{id}client + SSAR per actionproviders/kuery,providers/quickstart/graphql/{cluster}(pkg/hub/graphql.go){front-proxy}/clusters/{cluster}as the callerTwo behaviour changes to expect under
platform. A delegated token is pinned by kcp to one workspace, so a provider that was following the caller's bearer into a different workspace of theirs now gets a 403 — that is the point, but it is the shape any breakage will take. And the credential those three providers retain past the request expires in ten minutes instead of being a live hub token.Tests
pkg/hub/providers/proxy_delegation_test.go(new):off(unset policy, explicit off, off-with-exclusions) forwards the caller's bearer and mints nothing;platform/allswap it, never leakcallerBearer, keepX-Faros-User/X-Faros-Tenant, and ask the issuer for exactly(org, ws, user, provider)once; the exclusion list is honoured underplatform, ignored underall, matched exactly and trimmed; fail-closed for no issuer (503), mint error (503), org-scope caller with no workspace (403), unresolved caller (403), each asserting the upstream was never hit; anonymous probes still reach the provider with no credential; the UI proxy ignores the policy; plus table tests forParseDelegationModeandDelegatesPlatform.pkg/hub/providers/proxy_edge_test.go: comment onTestBackendProxyPlatformProviderStillReceivesCallerBearerupdated — it now pins the default configuration rather than "step C is future work". Assertions unchanged.providers/edges/internal/tunnel/auth_test.go(new — the package had no coverage ofauth.go): a delegated SA token from the tenant workspace resolves to a usable identity and passes theproxySAR on a Service the caller may reach, with both reviews going to the tenant VW rather than the provider's workspace and the SA's real groups kept; SAR denial and unauthenticated tokens are refused (no SAR issued in the latter);parseServiceAccountTokenshape table showing a bound delegated token is deliberately not a foreign SA (so it is reviewed in the workspace that minted it) while the legacy secret-backed shape is; foreign-SA re-qualification tosystem:kcp:serviceaccount:{home}:default:{name}with groups dropped; and a foreign token resolving to a human is refused.Commands, all green:
GOWORK=off go build ./...,GOWORK=off go vet ./pkg/...,GOWORK=off go test -count=1 ./pkg/hub/... ./pkg/server/...,cd providers/edges && GOWORK=off go build ./... && go vet ./... && go test -count=1 ./...,GOWORK=off make fix-lint && GOWORK=off make lint(0 issues).🤖 Generated with Claude Code