Summary
On the proxy runner middleware chain (both the operator/proxyrunner path used by MCPServer/MCPRemoteProxy and the thv run CLI path), a request denied by the authorization middleware returns HTTP 403 correctly but produces no audit event. Audit trails miss exactly the events they most need to record: denied requests.
Root cause
Both middleware chain builders in pkg/runner append the audit middleware after the authorization middleware:
PopulateMiddlewareConfigs (pkg/runner/middleware.go) — operator / proxyrunner path
WithMiddlewareFromFlags (pkg/runner/config_builder.go) — CLI path
The proxies wrap handlers in reverse slice order (applyMiddlewares), so earlier entries are the outermost wrappers. The effective request-time order is:
auth → telemetry → authorization → audit
Audit runs inside authorization. When Cedar denies, the authz middleware writes the 403 and returns without calling the inner handler — the audit middleware never executes, so no event is emitted.
The audit middleware already classifies these outcomes correctly (determineOutcome maps 401/403 → denied); it just never runs for denials.
Expected
Every request that reaches the audit middleware's position in the chain should produce an audit event, including policy denials (outcome denied). This is how the vMCP Serve path already behaves after #5841: its pre-dispatch authorization gate runs inside the audit middleware, so denied calls are audited.
Reproduction
- Run any MCP server through the proxy with
--authz-config (policy that denies a tool) and --audit-config.
- Call the denied tool directly via
tools/call.
- Observe: HTTP 403 returned, but no audit event is written; allowed calls produce events normally.
Fix
Move the audit middleware before authorization in both chain builders so audit wraps authz, and pin the ordering with tests (unit ordering tests plus a full-chain integration test asserting a denied tools/call yields 403 and an audit event with outcome denied).
Summary
On the proxy runner middleware chain (both the operator/proxyrunner path used by
MCPServer/MCPRemoteProxyand thethv runCLI path), a request denied by the authorization middleware returns HTTP 403 correctly but produces no audit event. Audit trails miss exactly the events they most need to record: denied requests.Root cause
Both middleware chain builders in
pkg/runnerappend the audit middleware after the authorization middleware:PopulateMiddlewareConfigs(pkg/runner/middleware.go) — operator / proxyrunner pathWithMiddlewareFromFlags(pkg/runner/config_builder.go) — CLI pathThe proxies wrap handlers in reverse slice order (
applyMiddlewares), so earlier entries are the outermost wrappers. The effective request-time order is:Audit runs inside authorization. When Cedar denies, the authz middleware writes the 403 and returns without calling the inner handler — the audit middleware never executes, so no event is emitted.
The audit middleware already classifies these outcomes correctly (
determineOutcomemaps 401/403 →denied); it just never runs for denials.Expected
Every request that reaches the audit middleware's position in the chain should produce an audit event, including policy denials (outcome
denied). This is how the vMCP Serve path already behaves after #5841: its pre-dispatch authorization gate runs inside the audit middleware, so denied calls are audited.Reproduction
--authz-config(policy that denies a tool) and--audit-config.tools/call.Fix
Move the audit middleware before authorization in both chain builders so audit wraps authz, and pin the ordering with tests (unit ordering tests plus a full-chain integration test asserting a denied
tools/callyields 403 and an audit event with outcomedenied).