fix(egress): scan response + websocket DLP against the resolved per-flow config #401

Merged
didericis-claude merged 2 commits from fix/websocket-response-dlp-multitenant into main 2026-07-17 17:16:30 -04:00
Collaborator

What

In the consolidated (multi-tenant) gateway, the egress addon's static self.config is empty — each request's real policy is resolved per-request from the orchestrator's /resolve. But response() and websocket_message() still matched routes against that empty self.config, so inbound prompt-injection DLP and WebSocket credential/injection DLP silently skipped every scan (fail-open) whenever the gateway ran multi-tenant. request() was already correct (it resolves per-flow); the two later hooks were not.

Closes finding #2 of the audit in #400 — and, as noted there, fixes the same root cause on the response path too, not only WebSocket.

Fix

Resolve the per-flow (config, slug, env) once in request(), stash it on flow.metadata, and have response() / websocket_message() read it back — falling back to the static single-tenant values for a flow that never passed through request(). Reusing the request's single /resolve avoids a round-trip per response and per WebSocket frame, and re-resolving later would be unreliable anyway (the identity token is stripped from the request during request()).

Backend-agnostic

The gateway image and this addon are shared by the Firecracker, macOS, and docker consolidated backends, so this one change fixes all of them; no backend-specific code is touched.

Tests

  • New TestMultiTenantInboundDlp: drives request() (resolves + stashes), then exercises response() and websocket_message() for a multi-tenant bottle — verified these fail against the old fall-open behaviour and pass with the fix.
  • Existing single-tenant response/websocket tests unchanged and green (the fallback preserves them).
  • pyright: 0 errors. pylint: 9.72/10. Full egress suites (199 tests) green. (The 13 test_gateway_init failures on the dev host are a pre-existing /bin/sleep path issue on NixOS, unrelated.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ

## What In the consolidated (multi-tenant) gateway, the egress addon's static `self.config` is empty — each request's real policy is resolved per-request from the orchestrator's `/resolve`. But `response()` and `websocket_message()` still matched routes against that empty `self.config`, so **inbound prompt-injection DLP and WebSocket credential/injection DLP silently skipped every scan (fail-open)** whenever the gateway ran multi-tenant. `request()` was already correct (it resolves per-flow); the two later hooks were not. Closes finding #2 of the audit in #400 — and, as noted there, fixes the same root cause on the **response** path too, not only WebSocket. ## Fix Resolve the per-flow `(config, slug, env)` once in `request()`, stash it on `flow.metadata`, and have `response()` / `websocket_message()` read it back — falling back to the static single-tenant values for a flow that never passed through `request()`. Reusing the request's single `/resolve` avoids a round-trip per response and per WebSocket frame, and re-resolving later would be unreliable anyway (the identity token is stripped from the request during `request()`). ## Backend-agnostic The gateway image and this addon are shared by the Firecracker, macOS, and docker consolidated backends, so this one change fixes all of them; no backend-specific code is touched. ## Tests - New `TestMultiTenantInboundDlp`: drives `request()` (resolves + stashes), then exercises `response()` and `websocket_message()` for a multi-tenant bottle — verified these **fail against the old fall-open behaviour** and pass with the fix. - Existing single-tenant response/websocket tests unchanged and green (the fallback preserves them). - `pyright`: 0 errors. `pylint`: 9.72/10. Full egress suites (199 tests) green. (The 13 `test_gateway_init` failures on the dev host are a pre-existing `/bin/sleep` path issue on NixOS, unrelated.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis-claude added the Kind/Security label 2026-07-17 16:22:48 -04:00
didericis added 2 commits 2026-07-17 17:15:13 -04:00
In the consolidated (multi-tenant) gateway the addon's static `self.config`
is empty — each request's real policy comes from the per-request `/resolve`.
`response()` and `websocket_message()` still matched routes against that empty
config, so inbound prompt-injection DLP and WebSocket credential/injection DLP
silently skipped every scan (fail-open) whenever the gateway ran multi-tenant.
This is backend-agnostic: the gateway image (and this addon) is shared by the
Firecracker, macOS, and docker consolidated backends.

Resolve the per-flow (config, slug, env) once in `request()`, stash it on
`flow.metadata`, and have both hooks read it back — falling back to the static
single-tenant values for a flow that never passed through `request()`. Reusing
the request's one `/resolve` avoids a round-trip per response and per WebSocket
frame.

Adds multi-tenant regression tests for both hooks that fail against the old
fall-open behaviour.

Refs: audit issue #400 (finding #2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
fix(egress): redact the per-bottle token in the now-active multi-tenant response log
test / unit (pull_request) Successful in 1m9s
test / integration (pull_request) Successful in 21s
test / coverage (pull_request) Successful in 1m24s
lint / lint (push) Successful in 2m15s
test / unit (push) Successful in 1m8s
test / coverage (push) Successful in 1m20s
test / integration (push) Successful in 24s
Update Quality Badges / update-badges (push) Successful in 1m18s
fd86e7fa99
Self-review of this PR: making `response()` run in the consolidated gateway
also activates its `LOG_FULL` `_log_response` call there — previously
unreachable, since the empty static config made `response()` return early. That
logger redacted with `os.environ`, which in multi-tenant mode does NOT hold the
bottle's per-request `/resolve` tokens (only the resolved `env` overlay does),
so a non-token-shaped provisioned secret appearing in a response could be logged
in the clear.

Thread the resolved per-flow `env` into `_log_request` / `_log_response` so the
LOG_FULL redaction scrubs the calling bottle's secrets. Adds a regression test
(a non-token-shaped `/resolve` secret, absent from os.environ, must not appear
in the response log) and updates the redaction-test helpers for the new arg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
didericis force-pushed fix/websocket-response-dlp-multitenant from 1afb78094b to fd86e7fa99 2026-07-17 17:15:13 -04:00 Compare
didericis-claude merged commit fd86e7fa99 into main 2026-07-17 17:16:30 -04:00
didericis-codex removed the Kind/Security label 2026-07-17 21:36:34 -04:00
Sign in to join this conversation.