Skip to content

feat(mcp): introduce injectionPolicy for MCP API key handling in per-backend security policy - #2663

Merged
nacx merged 8 commits into
theagentrouter:mainfrom
aish1331:inject-fwd-cred-header
Sep 14, 2026
Merged

nacx merged 8 commits into
theagentrouter:mainfrom
aish1331:inject-fwd-cred-header

Conversation

@aish1331

@aish1331 aish1331 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

Problem

When an MCP backend has both a security policy API key and forwardHeaders that land on the same header (typically Authorization but can be any custom header key), the gateway always overwrites the forwarded value with the configured credential.

That prevents a common setup: a default least-privilege service account on the backend, overridden by a caller-supplied personal access token when the client sends one. forwardHeaders is how clients inject that token today (usually via a dedicated header renamed with backendHeader), but the security policy wins and the backend only ever sees the service account.

Fix

This PR adds an overwrite field on backendRef.securityPolicy.apiKey so a default backend credential can coexist with a caller-supplied token forwarded onto the same header.

This update adds an InjectionPolicy enum to the MCPBackendAPIKey struct (backendRef.securityPolicy.apiKey), controlling whether the injected API key replaces existing values in the target header.
The default behavior is to overwrite the already present header (InjectionPolicy = Always), but when set to IfNotPresent, the API key is only injected if the header is absent, allowing for caller-supplied tokens to be preserved.

This change includes validation rules to ensure that InjectionPolicy cannot be IfNotPresent when using query parameters, as query-parameter injection always rewrites the backend URL.

Additionally, tests have been added to verify the behavior of the new InjectionPolicy field in various scenarios, ensuring correct handling of inline and secret reference API keys.

Changes:

  • Updated MCPBackendAPIKey struct to include InjectionPolicy field.
  • Added validation rules for InjectionPolicy in the CRD.
  • Enhanced controller logic to respect the InjectionPolicy setting.
  • Expanded unit and integration tests to cover new functionality.

Related Issues/PRs (if applicable)

Fixes #2661

Related Issues:

#1966

Special notes for reviewers (if applicable)

  • Default remains Always; this is opt-in per backend.
  • Only the Authorization header gets a Bearer prefix; custom headers (X-Api-Key, X-GitHub-Token, …) are injected verbatim.
  • If the MCPRoute itself uses OAuth or API-key client auth, callers must not forward inbound Authorization. Use a dedicated client header and backendHeader to map the backend token. This should be documented.

…hether the injected API key replaces existing values in the target header

Signed-off-by: Aishwarya <aishraimule@gmail.com>
Signed-off-by: Aishwarya <aishraimule@gmail.com>
@aish1331
aish1331 requested a review from a team as a code owner September 9, 2026 21:19
@netlify

netlify Bot commented Sep 9, 2026

Copy link
Copy Markdown

Deploy Preview for theagentrouter ready!

Name Link
🔨 Latest commit 669b00c
🔍 Latest deploy log https://app.netlify.com/projects/theagentrouter/deploys/6aa800be22a6560008d58d42
😎 Deploy Preview https://deploy-preview-2663--theagentrouter.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@aish1331 aish1331 changed the title feat(mcp): introduce Overwrite field for MCP API key handling in per per-backend security policy feat(mcp): introduce Overwrite field for MCP API key handling in per-backend security policy Sep 9, 2026
Comment thread api/v1beta1/mcp_route.go Outdated
//
// +kubebuilder:default=true
// +optional
Overwrite *bool `json:"overwrite,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default is to have overwrite to be true. The field does not make sense as it was the behaviour before the client override option.

We should rather have a field which describes whether the client set keys are allowed to be passed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, booleans with a default True are odd and confusing.
I have the following reasons to add a field here:

  1. There's only one BackendSecurity Policy header, forward headers is an array. The overwrite field on forwardHeaders would only be valid for 1 header at most.
  2. The client headers are populated earlier in the filter chain. Adding a overwrite there doesn't make sense from a code perspective, as there's nothing to be overwritten at that time. But that's an implementation detail, and users need not know about the order.

Instead of overwrite, I can use an enum injectionPolicy with values IfNotPresent and Always as the default. WDYT?

Signed-off-by: Aishwarya <aishraimule@gmail.com>
@mohitgurnani

mohitgurnani commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Nice fix @aish1331 — traces directly to the line we flagged on #2661 (ensureMCPBackendRefHTTPFilter's hardcoded Overwrite: ptr.To(true)), and I can see from the review thread with @gavrissh why injectionPolicy (enum) won out over a raw overwrite bool — makes sense given forwardHeaders is a list and credential injection runs as a separate, later filter stage from where headers get forwarded. Good to see inline keys routed through the credential-injection path under IfNotPresent too, and the e2e test that sends a wrong X-GitHub-PAT and asserts a hard failure is exactly the case worth proving.

Two things worth a look:

  1. api/v1alpha1/mcp_route.go's MCPBackendAPIKey doesn't get an InjectionPolicy field here, and the CRD still serves v1alpha1 (served: true) with no conversion webhook in the repo, so v1alpha1 users have no path to this at all. Intentional (v1beta1-only going forward), or worth adding there too for parity?

  2. injectionPolicy gives Always/IfNotPresent, but there's no third option to make the caller's credential required — i.e., no way to say "fail the request if the caller didn't forward their own token," rather than silently falling back to the configured key. The LLM backend side already has exactly this via BackendAuth.CredentialOverride.FallbackToConfigured (falseErrCredentialMissing → a real 401 from the gateway). Worth adding an equivalent third state here for callers who want to guarantee real user identity reaches the backend, not just prefer it when available?

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@nacx nacx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Ovewrall LGTM

@nacx

nacx commented Sep 14, 2026

Copy link
Copy Markdown
Member

Worth adding an equivalent third state here for callers who want to guarantee real user identity reaches the backend, not just prefer it when available?

I think this probably belongs more to the AutZ policy than the credential injection policy. If someone wants to enforce that, the right way would be to not configure credential injection and have an authz policy that requires the header.

@aish1331

Copy link
Copy Markdown
Contributor Author

api/v1alpha1/mcp_route.go's MCPBackendAPIKey doesn't get an InjectionPolicy field here, and the CRD still serves v1alpha1 (served: true) with no conversion webhook in the repo, so v1alpha1 users have no path to this at all. Intentional (v1beta1-only going forward), or worth adding there too for parity?

This was intentional: v1beta1-only going forward. v1alpha1.MCPRoute already carries the +kubebuilder:deprecatedversion warning pointing at v1beta1, and there's existing precedent for MCP surface landing only on the storage version - PrefixMode and PromptSelector are both v1beta1-only today.

injectionPolicy gives Always/IfNotPresent, but there's no third option to make the caller's credential required — i.e., no way to say "fail the request if the caller didn't forward their own token," rather than silently falling back to the configured key. The LLM backend side already has exactly this via BackendAuth.CredentialOverride.FallbackToConfigured (false → ErrCredentialMissing → a real 401 from the gateway). Worth adding an equivalent third state here for callers who want to guarantee real user identity reaches the backend, not just prefer it when available?

++ to @nacx's point. Moreover, reusing the CredentialOverride is not a great idea for the following reasons:

  1. MCP and LLMs have differents path for Security Policy/Credential Injection. LLM path involves extproc and the 3 checks happen in the handler. In case of MCP, we leverage native CredentialInjection HTTPRouteFilter which doesn't have the fail close option. So, we cannot easily implement this unless we want to move the handling to mcp proxy.
  2. Reusing the struct is also not a great idea because it has various other options like fromDynamicMetadata which are not valid for MCP as of now.

Having said that, these two behaviours (credential injection in MCP and credential override in LLM) are similar.
I would like to revisit this when we plan to move to a common BackendSecurityPolicy from the MCPBackend proposal.

@aish1331 aish1331 changed the title feat(mcp): introduce Overwrite field for MCP API key handling in per-backend security policy feat(mcp): introduce injectionPolicy for MCP API key handling in per-backend security policy Sep 14, 2026
@aish1331

Copy link
Copy Markdown
Contributor Author

/retest

…route - how headers are forwarded as-is and if you want to forward Authorization header you have to add the prefix (Bearer, Basic, etc) when you send the header

Signed-off-by: Aishwarya <aishraimule@gmail.com>
@nacx
nacx enabled auto-merge (squash) September 14, 2026 15:12
@nacx
nacx merged commit 5df7e60 into theagentrouter:main Sep 14, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow MCP backend API key injection to preserve a forwarded credential header

4 participants