mcp-chassis: validate a LIST of trusted issuers (accept k8s SA tokens) — ADR-0011 #77
Closed
opened 2026-07-06 19:31:42 +00:00 by mathias
·
5 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#77
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the one hard dependency of infra ADR-0011 (service-to-service auth: native identity per trust domain).
Problem
mcp-chassis
BearerMiddleware/JWTValidatormatchesissexactly against a singleDEX_ISSUER_URL. That's why every in-cluster caller falls back to a long-lived static bearer. ADR-0011's whole plan (in-cluster pods authenticate with kubelet-rotated, audience-bound k8s ServiceAccount tokens) hinges on the chassis trusting more than one issuer.Change
Make the validator accept a configurable list of trusted issuers, each with its own JWKS source and (optional) required audience:
https://kubernetes.default.svc.cluster.local, JWKS fromhttps://kubernetes.default.svc/openid/v1/jwks(in-cluster, cached) — for D1 SA tokens.Additive and backward-compatible: keep accepting the current Authentik/static bearers through the migration.
Proven viable (live-cluster spike, 2026-07-06)
k3s exposes OIDC discovery + audience-scoped
kubectl create tokenout of the box — no cluster-config change. Ajwx/v2verifier (the library the chassis already uses) validated a real SA token: accepts for its audience, rejects wrong-audience (replay block), rejects wrong-issuer. Verifier used:For a trusted-issuer LIST: pick the entry whose
issuermatches the token'siss(peek unverified), thenjwt.Parsewith that entry's JWKS + required audience. In-cluster, fetch+cache JWKS fromkubernetes.default.svc.Acceptance
JWTValidatoraccepts a[]TrustedIssuer{ Issuer, JWKSURL, Audience }(config/env).iss=https://kubernetes.default.svc.cluster.local+ matching audience validates; wrong-audience and wrong-issuer are rejected (unit tests).Then infra rolls SA-token projected volumes onto the in-cluster callers (ADR-0011 migration step 2+). Full rationale + spike detail: brain
wiki/homelab/decisions/service-to-service-auth-native-identity-per-domain-2026-07-06; infra ADR-0011.Done — shipped in mcp-chassis
v0.4.0(75eb8b8). Recommend close.Heads-up: this issue was misfiled — the chassis is its own repo
git.d-ma.be/mathias/mcp-chassis(module.../auth), not hyperguild. The change landed there.What shipped (TDD, backward-compatible)
IssuerConfig{IssuerURL, Audience}+NewMultiJWTValidator([]IssuerConfig).JWTValidatornow holds a list of issuer entries + a sharedjwk.Cache;Validatetries each trusted issuer, returns the subject on first success. All-issuers-JWKS-unreachable →ErrUnavailable(503); any definitive reject → 401.NewJWTValidator(single issuer) andBearerMiddlewaresignatures UNCHANGED → gitea-mcp / ingestion compile and behave identically; opting into multi-issuer is a one-line constructor swap.auth/jwt_test.go: an in-process OIDC issuer harness (discovery + JWKS + token minting) — the chassis had no happy-path JWT test before. New tests: accept-either-trusted-issuer, reject-untrusted (401 not 503), per-issuer audience enforcement, single-issuer backward compat. Full suite green (incl. all prior tests),-race/vet/gofmtclean.Acceptance
[]TrustedIssuer{Issuer, Audience}(IssuerConfig)Remaining (ADR-0011 migration step 2+, not this issue)
Bump the
mcp-chassisdep in gitea-mcp + ingestion; swap theirNewJWTValidator(...)→NewMultiJWTValidator(..., {IssuerURL: "https://kubernetes.default.svc.cluster.local", Audience: "<svc>"}); mount an audience-scoped projected SA token on one pod (issue-reconcile is the clean guinea pig) → prove D1 end-to-end through a live MCP server.Note there's no CI in mcp-chassis (
.gitea/workflowsabsent) — a shared auth library arguably wants a test gate; worth a separate issue.Recommend closing (chassis change complete); track the consumer wiring under ADR-0011.
Consumer wiring DONE + proven live on gitea-mcp
The remaining acceptance item ("gitea-mcp wired to trust the cluster issuer") is complete and verified end-to-end on the live server.
gitea-mcp (
240c3ec): bumped chassis v0.3.0 → v0.5.0; switchedNewJWTValidator→NewMultiJWTValidator(Authentik + k8s issuer); addedcmd/gitea-mcp/k8soidc.go(cluster-CA + SA-bearer client for the authenticated JWKS fetch). Additive + fallback: if the in-cluster OIDC is unreachable at startup the k8s issuer is dropped so Authentik/static auth is never affected. Smoke-tested off-pod (graceful skip).infra (
v0.27.0):K8S_ISSUER_URL+K8S_MCP_AUDIENCE=gitea-mcpon the deployment. Rolled via Flux; the live pod loaded both issuers with no fallback (authed JWKS fetch works from the real pod).Live proof: a real
aud=gitea-mcpSA token → the live gitea-mcp via the public ingress returned HTTP 200 (full MCPinitialize); no-token = 401; the claude-ai/static path stayed up throughout.gitea-mcp wired to trust the cluster issuer
Remaining is per-caller migration (give an in-cluster caller a projected
aud=gitea-mcptoken and drop its static bearer — issue-reconcile is the clean first) + optionally wiring ingestion/brain-mcp the same way. Tracked under ADR-0011, not this issue. This one is fully done — close it.🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run
🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
Closing — this was done and marked "recommend close" back on 2026-07-06 (both comments above), fully shipped in
mcp-chassisv0.4.0 + live-proven on gitea-mcp. My dispatch label just now was a mistake — I should have checked live state before labeling (same lesson as the missing_edge cluster earlier this sprint); it wasted a run for no reason since the actual work already existed. Closing for real this time. Session close, 2026-08-26.