Skip to content

security: strip untrusted identity headers when no auth backend configured#1446

Open
yossiovadia wants to merge 5 commits into
vllm-project:mainfrom
yossiovadia:fix/auth-header-validation
Open

security: strip untrusted identity headers when no auth backend configured#1446
yossiovadia wants to merge 5 commits into
vllm-project:mainfrom
yossiovadia:fix/auth-header-validation

Conversation

@yossiovadia
Copy link
Copy Markdown
Collaborator

@yossiovadia yossiovadia commented Mar 6, 2026

Summary

Fixes #1445 — without an auth backend, any client can spoof x-authz-user-id and x-authz-user-groups headers to impersonate any user, bypassing role-based routing, per-user rate limits, and memory isolation.

Fix

Add authz.trust_identity_headers config flag:

  • true (default): trust identity headers — assumes an auth backend (ext_authz, Envoy Gateway JWT, oauth2-proxy, etc.) injects them before the request reaches ext_proc. This preserves backward compatibility.
  • false: strip identity headers from every request, preventing client spoofing.

Deployments without an auth backend should add:

authz:
  trust_identity_headers: false

Why not auto-detect?

Identity headers can be injected by many mechanisms (ext_authz/Authorino, Envoy Gateway JWT claim_to_headers, oauth2-proxy, Istio RequestAuthentication). The router has no reliable way to distinguish "injected by auth backend" from "set by client" — by the time ext_proc sees the request, both look identical. An explicit config flag gives operators control without breaking existing deployments.

Changes

File Change
pkg/config/config.go Add TrustIdentityHeaders field + ShouldTrustIdentityHeaders() method
pkg/extproc/processor_req_header.go Strip identity headers when trust is disabled
e2e/testing/10-security-audit-test.py Security e2e tests (12 tests)

Test plan

  • test_spoofed_identity_headers_are_stripped — auth header test
  • All 12 e2e security tests pass
  • make build-router passes
  • golangci-lint — 0 issues on changed files
  • Default true preserves backward compatibility (existing authz-rbac integration test should pass)

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 6, 2026

Deploy Preview for vllm-semantic-router ready!

Name Link
🔨 Latest commit 37cfcf2
🔍 Latest deploy log https://app.netlify.com/projects/vllm-semantic-router/deploys/69c3d6f64124340008a54d48
😎 Deploy Preview https://deploy-preview-1446--vllm-semantic-router.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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 6, 2026

👥 vLLM Semantic Team Notification

The following members have been identified for the changed files in this PR and have been automatically assigned:

📁 config

Owners: @rootfs, @Xunzhuo
Files changed:

  • config/config.yaml

📁 e2e

Owners: @Xunzhuo
Files changed:

  • e2e/config/config.session-affinity-demo.yaml
  • e2e/config/envoy.session-affinity-demo.yaml
  • e2e/config/router-runtime.json
  • e2e/testing/10-security-audit-test.py

📁 src

Owners: @rootfs, @Xunzhuo, @wangchen615
Files changed:

  • src/semantic-router/pkg/config/config.go
  • src/semantic-router/pkg/extproc/processor_req_header.go

vLLM

🎉 Thanks for your contributions!

This comment was automatically generated based on the OWNER files in the repository.

@yossiovadia yossiovadia force-pushed the fix/auth-header-validation branch from 897ad3f to b2b4df0 Compare March 6, 2026 19:06
@yossiovadia
Copy link
Copy Markdown
Collaborator Author

Note: The authz-rbac integration test sends raw identity headers from the test client without JWT validation. This test passes with the default trust_identity_headers: true, but it means the test doesn't validate the full auth chain. Filed #1447 to track improving the test to use actual JWT validation.

…oofing (vllm-project#1445)

Without an auth backend, identity headers (x-authz-user-id, x-authz-user-groups)
come directly from the client and can be spoofed. This allows any client to
impersonate any user, bypassing role-based routing, per-user rate limits,
and memory isolation.
Fix: add authz.trust_identity_headers config flag (default: true for backward
compatibility). When set to false, the router strips identity headers from
incoming requests and logs a warning.
- Add TrustIdentityHeaders field to AuthzConfig (pointer for nil = default true)
- Add ShouldTrustIdentityHeaders() method
- Strip identity headers in handleRequestHeaders when trust is disabled
- Default true preserves backward compatibility with existing auth backend
  deployments (ext_authz, Envoy Gateway JWT, oauth2-proxy, etc.)
Deployments without an auth backend should set:
  authz:
    trust_identity_headers: false
Fixes vllm-project#1445

Signed-off-by: Yossi Ovadia <yovadia@redhat.com>
Signed-off-by: Yossi Ovadia <yovadia@redhat.com>
@yossiovadia yossiovadia force-pushed the fix/auth-header-validation branch from c6b3695 to 9dbad1d Compare March 18, 2026 19:48
yossiovadia and others added 3 commits March 18, 2026 13:29
@github-actions
Copy link
Copy Markdown
Contributor

✅ Supply Chain Security Report — All Clear

Scanner Status Findings
AST Codebase Scan (Py, Go, JS/TS, Rust) 27 finding(s) — MEDIUM: 21 · LOW: 6
AST PR Diff Scan No issues detected
Regex Fallback Scan No issues detected

Scanned at 2026-03-25T12:38:28.313Z · View full workflow logs

@rootfs rootfs requested review from Copilot and yehuditkerido March 25, 2026 12:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an explicit authz.trust_identity_headers configuration flag to prevent client spoofing of identity headers in deployments without an auth backend, by stripping configured identity headers at request header processing time.

Changes:

  • Introduces TrustIdentityHeaders config + ShouldTrustIdentityHeaders() defaulting to true for backward compatibility.
  • Strips configured identity headers from incoming requests when trust_identity_headers: false, with a warning log.
  • Adds a new e2e “security audit” test suite plus demo/e2e configs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/semantic-router/pkg/extproc/processor_req_header.go Strips identity headers when trust is disabled to mitigate spoofing.
src/semantic-router/pkg/config/config.go Adds config flag + helper for defaulting behavior.
e2e/testing/10-security-audit-test.py Adds security-oriented e2e tests (incl. identity header stripping scenario).
e2e/config/router-runtime.json Adds runtime JSON fixture for e2e/demo flows.
e2e/config/envoy.session-affinity-demo.yaml Adds Envoy demo config for session affinity/dynamic routing.
e2e/config/config.session-affinity-demo.yaml Adds router config for session affinity demo scenario.
config/config.yaml Sets trust_identity_headers: true explicitly in sample config.

Comment on lines +96 to +100
if ctx.Headers[userIDHeader] != "" || ctx.Headers[userGroupsHeader] != "" {
logging.Warnf("Stripped untrusted identity headers (%s, %s) — trust_identity_headers is false. "+
"Set authz.trust_identity_headers: true if an auth backend injects these headers.",
userIDHeader, userGroupsHeader)
delete(ctx.Headers, userIDHeader)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The stripping condition only triggers when the header values are non-empty. If a request includes the identity headers with an empty value, they won’t be deleted, leaving behavior dependent on downstream interpretation of “present but empty”. Strip based on key presence (map lookup) rather than value content, and delete each header independently if present.

Suggested change
if ctx.Headers[userIDHeader] != "" || ctx.Headers[userGroupsHeader] != "" {
logging.Warnf("Stripped untrusted identity headers (%s, %s) — trust_identity_headers is false. "+
"Set authz.trust_identity_headers: true if an auth backend injects these headers.",
userIDHeader, userGroupsHeader)
delete(ctx.Headers, userIDHeader)
_, hasUserID := ctx.Headers[userIDHeader]
_, hasUserGroups := ctx.Headers[userGroupsHeader]
if !hasUserID && !hasUserGroups {
return
}
logging.Warnf("Stripped untrusted identity headers (%s, %s) — trust_identity_headers is false. "+
"Set authz.trust_identity_headers: true if an auth backend injects these headers.",
userIDHeader, userGroupsHeader)
if hasUserID {
delete(ctx.Headers, userIDHeader)
}
if hasUserGroups {

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +96
userIDHeader := r.Config.Authz.Identity.GetUserIDHeader()
userGroupsHeader := r.Config.Authz.Identity.GetUserGroupsHeader()
if ctx.Headers[userIDHeader] != "" || ctx.Headers[userGroupsHeader] != "" {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Header names are case-insensitive at the HTTP layer, but ctx.Headers is keyed by exact string. This can allow bypass of stripping if incoming header keys and configured header names differ by case (e.g., X-Authz-User-Id vs x-authz-user-id) depending on how Envoy/ext_proc populates header.Key. Normalize header keys consistently (e.g., lowercasing keys when populating ctx.Headers and lowercasing configured header names before lookup/deletion).

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +99
logging.Warnf("Stripped untrusted identity headers (%s, %s) — trust_identity_headers is false. "+
"Set authz.trust_identity_headers: true if an auth backend injects these headers.",
userIDHeader, userGroupsHeader)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Logging a warning on every request that contains the headers can be abused for log amplification (especially under active probing/attack when trust_identity_headers is false). Consider rate-limiting/sampling this warning and/or emitting a metric counter for stripped headers; optionally include ctx.RequestID to aid investigation without increasing log volume.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +88
def test_spoofed_identity_headers_are_stripped(self):
"""Spoofed auth headers should be stripped when no auth backend is configured."""
self.print_test_header(
"Auth Header Stripping",
"Spoofed x-authz-user-id should be stripped without auth backend",
)

r = eval_text("hello")
self.assertEqual(r.status_code, 200)

# The eval API doesn't expose which headers were stripped, but the
# router log confirms: "Stripped untrusted identity headers"
# The fix prevents role_bindings from matching spoofed identities,
# per-user rate limits from being bypassed, and memory isolation
# from being circumvented.
self.print_test_result(
True,
"Eval completed normally (identity headers stripped by router if present)",
)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This test doesn’t actually send spoofed identity headers, nor does it assert that stripping occurred (it only asserts a 200). To validate the fix, send a request that includes the configured identity headers (matching Authz.Identity header names) and assert an observable outcome—e.g., assert on captured router logs/metrics, or route behavior that depends on user identity—so the test fails if headers are not stripped.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +90
// When false, the router strips identity headers on every request and logs
// a warning. This prevents user impersonation via role_bindings, rate limits,
// and memory isolation.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The comment says it “logs a warning” on every request, but the implementation only logs when one of the identity headers is present. Update the comment to reflect actual behavior (or change logging behavior if the intent is truly “always log”).

Suggested change
// When false, the router strips identity headers on every request and logs
// a warning. This prevents user impersonation via role_bindings, rate limits,
// and memory isolation.
// When false, the router strips identity headers from incoming requests.
// If identity headers are present and stripped, it logs a warning. This
// prevents user impersonation via role_bindings, rate limits, and memory
// isolation.

Copilot uses AI. Check for mistakes.
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.

security: identity headers can be spoofed without ext_authz

5 participants