policy
On this page
Embeds the Praxis Policy Engine in-process to enforce multi-source identity, APL route policy, RFC 8693 token exchange, field redaction, session taint, audit emission, and (under body_access: read_write) request / response body rewriting. Content scanning is a host plugin the engine dispatches, not a bundled one.
Requires Cargo feature: policy-engine.
Configuration Notes
Registered under the YAML filter name policy. The policy-engine cargo feature is on by default; --no-default-features leaves the filter out.
A single request can carry multiple identity sources — user JWT in Authorization, agent JWT in X-Agent-Token, workload JWT in X-Workload-Token, etc. Each registered identity plugin reads its own configured header and contributes to a typed Extensions context.
On the body phase, the filter consumes protocol classifier filter metadata (from the praxis-ai package) to dispatch the matching CMF hook chain. APL routes (declared in the policy document) gate the tool/prompt/resource call by role, attribute, or Cedar PDP decision. delegate(...) steps mint audience-scoped tokens (RFC 8693) that the allow path attaches as upstream headers.
Policies with llm: routes authorize the top-level request model through cmf.llm_input, without classifier metadata. Missing, unlisted, and ambiguous models fail closed by default.
body_access: read_write enables the JSON-RPC re-serialization round-trip so APL field mutators (redact(), assign()) rewrite the upstream request body and the downstream response. It also enables cmf.llm_output for non-streaming inference responses. APL field mutators do not rewrite inference bodies.
Response-body hooks run on a small dedicated runtime while the worker waits, for at most twice the engine’s per-plugin timeout (engine_settings.plugin_timeout, so 60 seconds by default). A hook that does not finish in time is aborted and the response fails under the filter’s failure_mode: closed truncates the response, open passes the body through unfiltered.
Outbound policy calls share the proxy’s sub-request limits and circuit breaker, use HTTP/1.1, and keep a separate 1 MiB response ceiling. Calls made by response-body hooks use their own pool on the dispatch runtime, with the same connection limit but no circuit breaker. TLS uses the platform trust store; cluster private CAs and client certificates do not apply. A private destination requires trusted_private_endpoints for a specific host, or allow_private_idp to relax every callout.
An endpoint URL may name an IP address over http, but not over https: an IP carries no SNI, and Pingora peers skip certificate verification entirely when SNI is empty. Use a hostname for https.
Praxis filter configs are flat: the filter’s typed fields sit directly under the - filter: entry alongside the structural keys (name, conditions), not nested under a config: wrapper. See examples/configs/security/policy.yaml for a runnable example.
The referenced YAML is the policy document — plugins, routes, and identity-source declarations. The filter loads it once at construction and rejects misconfigured policy at server startup (fail-fast rather than at first request).
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
body_access | read_only | read_write | no | Body-access tier. ReadOnly (default) lets APL inspect request and response bodies for routing / policy decisions but discards any mutations. ReadWrite enables the CMF → JSON-RPC re-serialization round-trip so APL field mutators (e.g. args.ssn: redact(!perm.view_ssn)) rewrite the upstream body and response. Pay the round-trip cost only when needed. |
config_path | string | yes | Filesystem path to the policy document. |
init_timeout_secs | integer | no | Maximum time, in seconds, to wait for PolicyEngine::initialize at filter construction. Identity plugins fetch JWKS over HTTPS during init; a reachable-but-unresponsive identity provider would otherwise hang startup or hot-reload indefinitely. On expiry, filter construction returns an error and the server fails fast. 30s is generous for legitimate cold-cache JWKS fetches over the public internet, while short enough that misbehavior is noticed during the deploy. |
max_buffer_bytes | integer | no | Maximum request or response body size in ReadWrite mode. Also used as the default inference request limit. |
allow_private_idp | bool | no | Permit every policy endpoint a private or loopback address. By default, private DNS answers are skipped and calls with no public answer are rejected, so a private destination needs one of two opt-ins: trusted_private_endpoints to relax a specific host to the RFC 1918 and unique-local ranges, or this flag to relax every callout globally. Proxy upstreams use insecure_options.allow_private_endpoints instead. |
trusted_private_endpoints | string[] | no | Hosts the policy engine may reach at a private address. Narrower than allow_private_idp. Only a callout whose host matches an entry reaches a non-public address, and only the RFC 1918 and unique-local (fc00::/7) ranges an in-cluster endpoint resolves to. A listed host still cannot reach loopback, link-local (including cloud metadata), the unspecified address, or any other reserved range. Every unlisted callout stays public-only. Matched on the URL host, case-insensitive, port excluded. An IPv6 address is a bracketed literal such as [fc00::1]. Shared address space (100.64.0.0/10) is deliberately not relaxable, so a pinned endpoint on a cluster that assigns pod addresses there, such as EKS with the VPC CNI secondary-CIDR pattern, is not reachable by a pin. Relaxing that range would need a separate per-host opt-in, off by default. |
require_protocol_metadata | bool | no | Fail-closed policy gate for misconfigured chains. When true (default), on_request_body rejects any request that reaches it without mcp.method filter-metadata. The metadata is set by the protocol classifier filter (available in the praxis-ai package), so its absence means either (a) the protocol classifier filter is missing from the chain, or (b) it is ordered AFTER policy instead of before. Either is a misconfiguration that would silently bypass CMF/APL policy. Set to false only when intentionally fronting non-classified traffic through the policy filter for identity-only enforcement (legacy behavior). Only applies to policies with MCP entity routes. Inference routes use their own gates instead. JSON-RPC methods that legitimately carry no entity (e.g. tools/list, initialize, prompts/list) still pass; require_protocol_metadata only rejects when the metadata is missing entirely. |
llm | LlmOptions | no | Inference authorization options. |
llm.max_request_bytes | integer | no | Maximum buffered inference request size, in bytes. Requests over this limit receive HTTP 413. |
llm.promote_params | string[] | no | Top-level scalar fields promoted to custom.llm.<name>. A configured list replaces the defaults. |
llm.provider | string | no | Operator-supplied provider recorded on llm.provider. |
llm.require_model | bool | no | Deny a request whose body carries no usable top-level model. Enabled by default. When disabled, the request falls through to other policy paths. |
llm.require_route | bool | no | Deny a model no llm: route selects. Enabled by default. Disable only to admit unlisted models. |
Example
filter: policy
config_path: /etc/praxis/policy.yaml
body_access: read_write # optional; default read_only
require_protocol_metadata: true # optional; default true
init_timeout_secs: 30 # optional; default 30
max_buffer_bytes: 10485760
llm:
require_model: true
require_route: true
provider: openai