feat(filter): add identity header guard filter - #709
Conversation
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>
leseb
left a comment
There was a problem hiding this comment.
would core be more suitable for this instead of this repo? it's not so much "ai" related
praxis-bot
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
[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.
| ); | ||
| praxis_filter::register_filters!( | ||
| @register registry, | ||
| http "identity_header_guard" => IdentityHeaderGuardFilter::from_config |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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.
|
Flagging a likely conflict: this filter's 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 |
Summary
identity_header_guardfilter that captures headers matching a configurable prefix intofilter_metadataand strips them before upstream forwardingx-tenant-username,x-tenant-group) from leaking to LLM providersexternal_metering(feat(filter): add external metering filter for usage reporting and balance checks #577)Fixes #698
Motivation
The
external_meteringfilter 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_headersin core only handles hardcodedx-praxis-*prefixes with no metadata capture and no configurable prefixes (core TODO #186).Design
x-tenant-)filter_metadataunder a configurable namespace (prevents collision with verified auth metadata)request_headers_to_removeWhat's included
filters/src/identity_guard/— filter, config, 11 unit tests + 1 doctesttests/integration/tests/suite/examples/identity_header_guard.rs— 3 integration tests (config parse + header capture + strip)examples/configs/identity-header-guard.yaml— example configTest plan
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 warningsexternal_meteringconsuming captured identity