Add passthroughHeaders to VirtualMCPServer for header forwarding#5466
Add passthroughHeaders to VirtualMCPServer for header forwarding#5466juancarlosm wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5466 +/- ##
==========================================
+ Coverage 69.72% 69.74% +0.01%
==========================================
Files 645 646 +1
Lines 65598 65697 +99
==========================================
+ Hits 45737 45818 +81
- Misses 16517 16531 +14
- Partials 3344 3348 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
0d98f3a to
508a72a
Compare
Large PR justification has been provided. Thank you!
|
✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review. |
Move the forwarded-header carrier off auth.Identity (and out of the auth factory) onto a typed context value in pkg/vmcp/headerforward, wired in the vMCP server chain via Config.PassthroughHeaders. Addresses @jerm-dro's review on stacklok#5466. Behavior unchanged; validated end-to-end on a live cluster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jerm-dro
left a comment
There was a problem hiding this comment.
New direction LGTM — just a few minor notes inline.
A forwarded passthrough header whose name also appears in a backend's static header-forward config (plaintext or secret) is an admin misconfiguration. Return an error at session creation instead of silently picking a winner, so the conflict surfaces and can be fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the PassthroughHeaders doc comment to match the post-refactor capture/consume flow, and restore the unrelated auth-middleware comment in incoming.go to its pre-PR form to keep this PR scoped to the feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forwarded headers are captured once at backend-session creation. Add a second tool call with a changed caller header and assert the backend still observes the original value, locking that contract against a regression that re-reads headers per request or drops them after the first call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gh-headers # Conflicts: # pkg/vmcp/cli/serve.go # pkg/vmcp/server/server.go
3aea11e to
997ecfc
Compare
The main merge brought both the forwarded-header capture branch and upstream's rate-limit branch into Handler, pushing gocyclo to 16 (>15). Extract the capture block into applyForwardedHeaderCapture, mirroring applyRateLimiting, restoring complexity to 15. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PassthroughHeaders doc reword in pkg/vmcp/config feeds the VirtualMCPServer CRD's spec.config.passthroughHeaders description. Regenerate the CRD manifests and API docs so the generated output matches the source comment (fixes the Operator CI codegen checks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — pushed a round addressing all the notes:
Also merged latest Follow-up (separate PR): fail loud when a passthrough header collides with a backend's outgoing-auth strategy that manages the same header. |
Summary
When a client calls vMCP with a header its backend needs — e.g.
x-mcp-api-keythat the backend resolves to a specific user — vMCP silently drops it. vMCP opens a fresh request to each backend and sends only its own headers, so the caller's header never arrives. Backends that authenticate per-user from a header therefore can't be used through vMCP today — only by hitting the backend directly.This adds
spec.passthroughHeaders: an allowlist of incoming header names that vMCP forwards, unchanged, to every backend it calls.A listed header is read from the client request and attached to that session's backend requests. Headers not listed are ignored. Restricted names (
Host,Authorization,X-Forwarded-*, hop-by-hop) are rejected.Approach discussed and agreed with @jerm-dro on #3958 (assigned to me there).
Closes #3958
Type of change
Test plan
test/integration/vmcp/passthrough_headers_test.go): real client → capture → backend; asserts allowlisted header forwarded, non-allowlisted droppedgolangci-lintclean; CRD/deepcopy/docs regenerated (no drift)Does this introduce a user-facing change?
Yes:
Special notes for reviewers
*auth.Identityat the incoming-auth boundary (cloned, not mutated) and injected by the per-session backend client — it flows as an explicit parameter, not a context value, so novmcp-anti-patterns§1/§7 coupling and the round-tripper is unchanged. This is the no-context-plumbing path discussed in vMCP: add header_passthrough outgoing auth strategy to forward dynamic incoming headers to backends #3958 once the session refactor landed; thevmcp-reviewaudit was clean.(iss, sub)tuple is persisted — forwarded headers aren't, same as bearer tokens. Low impact withsessionAffinity: ClientIP.Large PR Justification
Most of the diff is required tests (~700 lines: unit + an in-process e2e) and generated code that can't be split (CRD manifests in two API versions × two chart copies, deepcopy, API docs). Hand-written source is ~240 lines. Already trimmed from ~1157 to ~1030 added lines by de-duplicating tests (coverage unchanged); the remainder is cohesive single-feature code that wouldn't be safe to split.