Skip to content

feat(filter): add identity header guard filter - #709

Open
yossiovadia wants to merge 1 commit into
praxis-proxy:mainfrom
yossiovadia:feat/identity-header-guard
Open

feat(filter): add identity header guard filter#709
yossiovadia wants to merge 1 commit into
praxis-proxy:mainfrom
yossiovadia:feat/identity-header-guard

Conversation

@yossiovadia

Copy link
Copy Markdown

Summary

Fixes #698

Motivation

The external_metering filter reads tenant identity from request headers for per-user usage attribution. These headers are set by an upstream auth layer and must not reach the upstream provider. reserved_headers in core only handles hardcoded x-praxis-* prefixes with no metadata capture and no configurable prefixes (core TODO #186).

Design

  • Configurable header prefix (default: x-tenant-)
  • Captured headers written to filter_metadata under a configurable namespace (prevents collision with verified auth metadata)
  • Matched headers marked for removal via request_headers_to_remove
  • ~120 lines of filter code

What's included

  • filters/src/identity_guard/ — filter, config, 11 unit tests + 1 doctest
  • tests/integration/tests/suite/examples/identity_header_guard.rs — 3 integration tests (config parse + header capture + strip)
  • examples/configs/identity-header-guard.yaml — example config
  • Generated filter docs and README updates

Test plan

  • 11 unit tests covering: prefix matching, case insensitivity, namespace isolation, no-match passthrough, empty/missing headers, multiple captures, strip verification
  • 1 doctest
  • 3 integration tests (config parse, header capture to metadata, upstream strip)
  • cargo xtask lint-example-tests — passes (example config has test coverage)
  • cargo xtask lint-filter-docs — passes (generated docs up to date)
  • cargo clippy -p praxis-ai-filters -- -D warnings — zero warnings
  • Validated end-to-end on OpenShift deployment with external_metering consuming captured identity

Captures request headers matching a configurable prefix into
filter_metadata and strips them before upstream forwarding.
Prevents identity headers (e.g. x-tenant-username, x-tenant-group)
from leaking to LLM providers while making them available to
downstream filters like external_metering.

Fixes praxis-proxy#698

Signed-off-by: Yossi Ovadia <yovadia@redhat.com>
@yossiovadia
yossiovadia requested review from a team and aslakknutsen August 11, 2026 14:46

@leseb leseb left a comment

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.

would core be more suitable for this instead of this repo? it's not so much "ai" related

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean implementation — the filter logic is correct and the security-critical behavior (stripping before forwarding, namespaced metadata to avoid collision with verified auth) is well thought out. Three medium findings, all related to test coverage gaps.

}

// -----------------------------------------------------------------------------
// Behavior Tests

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] This test claims to verify the default namespace but only asserts filter.name(), which is a static string unrelated to namespace selection. If the default were changed from "identity" to anything else, this test would still pass.

The behavior is tested indirectly by captures_matching_headers_to_metadata (which checks for the identity. prefix in metadata keys), but this named test is misleading — it guards nothing.

Convert to an async test that exercises a header through on_request and asserts the metadata key starts with identity., or remove it and let captures_matching_headers_to_metadata serve as the canonical default-namespace test.

Comment thread filters/src/register.rs
);
praxis_filter::register_filters!(
@register registry,
http "identity_header_guard" => IdentityHeaderGuardFilter::from_config

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] The build_ai_registry_includes_ai_and_builtin_filters test at the bottom of this file does not assert that identity_header_guard is present in the registry. Every other filter category added to this function has a corresponding assertion in that test. Add:

assert!(
    names.contains(&"identity_header_guard"),
    "expected identity_header_guard in registry"
);

// Namespaced key only. The guard must NOT write
// unnamespaced keys — jwt_auth writes those from
// verified claims, and overwriting them here would
// launder client-spoofed headers into the trusted

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] The if let Ok(val) guard correctly skips capturing non-UTF-8 values while the request_headers_to_remove push on line 114 still strips them — exactly the right security behavior. However, there is no unit test covering this edge case.

Add a test that inserts a matching-prefix header with a non-UTF-8 value (HeaderValue::from_bytes(&[0x80]).unwrap()), runs on_request, and asserts the header is in request_headers_to_remove but absent from filter_metadata. For a security guard filter, this divergent code path deserves explicit coverage.

@jordigilh

Copy link
Copy Markdown

Flagging a likely conflict: this filter's x-tenant- prefix capture-and-strip overlaps with #581's external_metering filter, which expects the same x-tenant-* header convention (username/group/subscription/model) and does its own stripping of those headers. Neither PR references the other.

If both land as-is, the pipeline would end up with two filters independently capturing/stripping the same headers into different metadata shapes. Might be worth the two of you syncing on which owns the canonical filter_metadata namespace before either merges — cc @noyitz.

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.

feat(filter): add identity header guard filter

4 participants