vMCP: passthroughHeaders blocks Authorization/Cookie even for backends whose outgoing-auth strategy sets nothing (unauthenticated)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Problem
Upgrading to v0.44.0 sent every VirtualMCPServer/MCPGroup in one of our clusters into CrashLoopBackOff on passthroughHeaders[0]: "Authorization" is a restricted header and cannot be forwarded.
pkg/vmcp/config/validator.go unconditionally rejects Authorization and Cookie in spec.passthroughHeaders, even for unauthenticated backends. unauthenticated.Authenticate() is a no-op — it sets no header — so nothing can ever collide with a passthrough entry there. Blocking it protects against nothing.
Use cases
- External auth backends. A backend sits behind its own OAuth/API-key gateway, unrelated to any vMCP strategy. The client already holds a valid
Authorizationthat just needs relaying untouched — the original ask behindpassthroughHeaders(#5466,#3958). - Gateway-mints, backend-verifies (zero-trust chains). A frontend (e.g. LiteLLM as an MCP gateway) mints a short-lived, audience-scoped token per request; the backend verifies it itself. See LiteLLM's own MCP zero-trust docs for the shape.
In both, vMCP's resolved strategy for that backend is unauthenticated — it injects nothing, so there's no vMCP-set header for the passthrough value to collide with.
Security questions
Isn't forwarding Authorization/Cookie inherently risky?
No — the default is deny. Every header is stripped unless the operator does two explicit things: lists it in spec.passthroughHeaders, on a backend whose resolved strategy happens to be unauthenticated. There's no implicit path — nothing forwards by accident. This issue doesn't touch that default; it only fixes when the extra block fires, for the one case (unauthenticated) where it protects nothing.
Doesn't this weaken the existing collision protection?
No. The real risk — vMCP's own resolved strategy sets the same header the operator listed, silently overwriting it (token_exchange/upstream_inject/aws_sts/obo/xaa all set Authorization; header_injection sets its configured name) — stays blocked exactly as today (#5535 tracks making that check itself more precise). This issue only covers unauthenticated, where the collision can't structurally happen: nothing is set, so nothing gets overwritten.
Is there precedent?
Yes — the standalone (non-vMCP) header-forward middleware (pkg/transport/middleware/header_forward.go:23-46) already allows Authorization (warning-logged) and never restricted Cookie. The blanket vMCP block is the outlier, tightened in #6235 with no carve-out for the no-op case.
Proposed fix
Same restricted-name list, same content — only where and when it's checked moves.
Today's check runs at config-parse time, before any backend is discovered; in "discovered" mode each strategy resolves later, per MCPServer, so there's no backend yet to ask "are you unauthenticated?" The check can't just gain a conditional — it has to move to where a backend's strategy actually exists: backendDiscoverer.Discover (pkg/vmcp/aggregator/discoverer.go), the one function both static and dynamic discovery return through, where AuthConfig gets populated. There, once per backend:
AuthConfig == nil→ skip, nothing to collide with.AuthConfig.Type == unauthenticated→ skip too. Distinct from nil: an operator can set it explicitly, or discovery can resolve to it. Toolhive's own backend-client code already treats both as equivalent (defaults to unauthenticated before checking nil), so the check must test both — nil alone would miss the explicit case.- Anything else → checked against the restricted list as today. Fails closed: any future strategy type is blocked by default unless it's
unauthenticated.
With this per-backend check in place, validatePassthroughHeaders's unconditional reject is redundant and comes out — it can't be correct on its own once the answer depends on info it doesn't have yet. Host, hop-by-hop, and X-Forwarded-* are untouched — they don't depend on any backend's strategy.
Ask
Implements #5535's intent, extended to Cookie for the same reasoning — #6235 blocked both under one justification that doesn't hold once unauthenticated sets nothing and forwarding is already explicit opt-in. Happy to open the PR with the concrete diff.
Co-authored with Claude Code.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with pkg/vmcp/config/validator.go and backendDiscoverer.Discover in pkg/vmcp/aggregator/discoverer.go, tracing where AuthConfig is populated for static and discovered backends. Remove the unconditional restricted-header rejection and enforce it per backend, skipping nil or unauthenticated strategies while retaining the existing restrictions for other strategies. Done means Authorization and Cookie work for explicitly or implicitly unauthenticated backends while Host, hop-by-hop, X-Forwarded-*, and collision protection remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100