Skip to content

[P0] Preserve bounded end-to-end HTTP headers across the gateway proxy #440

Open

Description

@seonghobae

Verified buyer/security gap

Fresh protected-source review on main@f8260f1e03836039ff9463dd99fa982e4e270c4b found that Wardnet's generic gateway is not yet an HTTP-semantics-preserving reverse proxy.

gateway() receives the inbound HeaderMap, but uses it only for client-IP attribution. It then calls proxy_request(&state, &route, &method, gateway_path, uri.query(), body) without the request headers. proxy_request() constructs the upstream request from only method, target URL, and body. On the return path it reads only the upstream status and complete body bytes and emits (status, bytes), so all upstream response fields are discarded.

That means a buyer route can lose required end-to-end semantics such as request Content-Type / Accept and response Content-Type, Location, Retry-After, authentication challenge or other explicitly admitted application metadata. A JSON API can therefore receive bytes without the caller's media type, and a client can receive upstream bytes/status without the upstream representation metadata.

A naive "forward every header" repair would create a separate security defect. Wardnet must never relay management credentials such as X-Admin-Token, attacker-controlled forwarding identity, Host, framing fields, proxy credentials, hop-by-hop fields, or any field named by Connection. Header mediation must therefore be explicit, bounded and independently testable.

Ownership boundary

This is Wardnet-owned generic gateway / SOC control-plane HTTP mediation. It does not move reusable outbound URL/address/DNS/peer/redirect/proxy/TLS/resource authorization from EgressWeave and must not copy EgressWeave source. It does not overlap #89's contextual-orchestrator-specific LLM seam, #435's Coraza enforcement sidecar contract, or #180/#181 route segment selection. The generic gateway should expose one narrow end-to-end-header policy that those specialized seams may consume without becoming duplicate transport-policy authorities.

Hostile realistic RED

Create a serialized Draft repair only after re-reading the then-current gateway parents and active writers. First keep production source byte-identical and use a loopback upstream fixture that proves the current behavior:

  • a benign JSON request with Content-Type: application/json and Accept: application/json reaches the upstream without those fields today; the acceptance contract requires the explicitly admitted end-to-end fields to arrive unchanged;
  • an upstream JSON response with Content-Type: application/json plus a bounded application response field reaches the Wardnet client without those fields today; the acceptance contract requires the admitted response fields to survive;
  • hostile request fields X-Admin-Token, X-Forwarded-For, X-Real-IP, Proxy-Authorization, Host, Connection, Transfer-Encoding, TE, Trailer, Upgrade and any field nominated by Connection must never be forwarded merely because header preservation is added;
  • hostile upstream hop-by-hop / Connection-nominated response fields must never be reflected to the client;
  • duplicate/sensitive/oversized header cases fail closed according to the bounded policy rather than being silently merged into a different security meaning;
  • a genuine required end-to-end field remains distinguishable from a stripped security-sensitive field in SOC/audit evidence;
  • existing route selection, local threat/DNSBL denial, Coraza authority and body-size gates remain unchanged.

Runner/bootstrap/format noise is not semantic RED. The test must reach the real loopback upstream/client assertions.

Minimum causal GREEN contract

Implement one Wardnet-owned header-mediation boundary rather than ad-hoc copies in each adapter.

  • Define an explicit bounded request/response end-to-end field policy. Default behavior must be least-authority; authorization/cookie or other credential-bearing propagation must require an explicit reviewed route/application contract rather than blanket passthrough.
  • Strip RFC hop-by-hop fields and every field dynamically nominated by Connection on both request and response paths.
  • Strip Wardnet management/authentication fields, untrusted forwarding identity, proxy credentials, Host and message-framing authority regardless of application allowlists.
  • Preserve exact field multiplicity where protocol semantics require it; do not comma-join fields whose grammar does not permit that transformation.
  • Keep outbound destination/DNS/peer/redirect/proxy/TLS authorization delegated to released EgressWeave contracts; header mediation cannot authorize transport.
  • Do not introduce cross-service SQL, mutable sibling dependencies, source copy, ambient proxy inheritance or a second LLM/provider-routing boundary.
  • Keep response streaming as the target architecture; do not treat the current whole-body buffering behavior as a reason to broaden this slice. If streaming is still independently unresolved, record it as a dependent gateway gap rather than hiding it.

Exact-head acceptance

On one unchanged candidate require cargo fmt --check, locked workspace tests, strict Clippy, Fuzz, security/SAST/CodeQL under live governance, current reviews/threads, 100% owned-production statement/branch/edge/public-rustdoc evidence for the new mediation code, and a realistic async loopback buyer path that remains within Wardnet's standing p95 <=20 ms target.

Keep the issue open until the effective code/test/doc/traceability delta reaches protected main through normal non-force governance. No self/model approval, force push, destructive rebase, routine bypass, gate weakening, no-op redispatch churn, predecessor-evidence promotion or release claim from a Draft head.

TRACEABILITY

Activity

  1. seonghobae commented on Sep 21, 2026

    @seonghobae
    ContributorAuthor

    2026-09-21 KST RED-lane repair update for #441.

    Exact d8fca38bdb57a10d8cfffc69e8b18b2ceef43427 did acquire a hosted runner in CI 35521507793, but rust job 106106284520 failed at Check formatting; test and Clippy never ran. That specimen is therefore not semantic RED and not a runner defect.

    Minimum repair changed only canonical rustfmt import ordering in tests/gateway_header_mediation.rs; production source remains byte-identical to protected main@f8260f1e03836039ff9463dd99fa982e4e270c4b. Current #441 exact head is 766e11d3427a062861511619e3b5d919ada6409c. Exact-head CI 35597826514, Security 35597826609, and SAST 35597826518 remain queued; CodeQL 35597826488 remains pending. No semantic RED/GREEN is claimed and no predecessor status transfers.

    The unchanged-head runner-admission specimen is already handed to canonical central owner ContextualWisdomLab/.github#712; this leaf must not add wake/no-op commits, blind redispatch, local workflow copies, synthetic status, gate weakening, or bypass.

    Source repair remains serialized behind the active src/lib.rs owners #435/#140. When this unchanged test-only head reaches the real loopback assertions, preserve that hosted semantic RED before adding one bounded mediation boundary plus hostile strip/fail-closed cases. Keep EgressWeave transport authority and other foreign-owner logic out of this lane.

  2. seonghobae commented on Sep 22, 2026

    @seonghobae
    ContributorAuthor

    2026-09-22 KST exact-current RED update for serialized #441.

    Hosted CI 35679113962 / rust job 106591966349 checked out the PR merge candidate for exact branch head 70f5aaa2d8b7c841097b78e2397943475cca71bb, passed rustfmt and all 145 library unit tests, then ran the real loopback gateway_header_mediation integration binary. Five of seven hostile cases failed semantically:

    • duplicate X-Admin-Token: upstream accepted request (202) instead of Wardnet failing closed (400);
    • duplicate singleton Content-Type: upstream accepted request (202) instead of 400;
    • admitted request Content-Type: application/json was dropped (None upstream);
    • admitted upstream Content-Type: application/json was collapsed to synthesized application/octet-stream;
    • admitted x-wardnet-app-meta >16 KiB was forwarded (202) instead of failing closed (400).

    The two stripping cases pass because current production proxying drops essentially all request/response metadata; they are not positive evidence that bounded mediation exists. Clippy was skipped only because the semantic test step failed. Production source on this head remains byte-identical to parent #435 exact e7e9d9b87a03c05f802a3d50c0917f75a89f7022.

    Minimum causal GREEN remains one Wardnet-owned generic HTTP mediation boundary: explicit request/response allowlists with multiplicity preservation; dynamic Connection nomination stripping; unconditional stripping of management credentials, blanket auth/cookies, forwarding identity, proxy credentials, attacker Host, framing and hop-by-hop fields; duplicate singleton and over-budget admitted request metadata fail closed before outbound I/O; malformed/ambiguous upstream singleton or over-budget admitted response metadata fails as upstream error. Do not add EgressWeave transport-authorization semantics, Coraza authority, streaming/backpressure (#443), or mutable sibling logic to this fix.

    After the production fix, require fresh exact-head fmt + locked workspace tests + strict Clippy and the hosted loopback hostile cases GREEN before advancing; predecessor GREEN does not transfer.

  3. seonghobae commented on Sep 22, 2026

    @seonghobae
    ContributorAuthor

    @jules Continue the now-proven RED only through the existing serialized writer PR #441 (test/440-gateway-header-mediation-red), current exact head e32a620fa66232405adbd1603327bb5abcf55b4a stacked on #435 exact e7e9d9b87a03c05f802a3d50c0917f75a89f7022. Append ordinary commits only; do not force-push, rebase destructively, open a parallel src/lib.rs source PR, or touch #140. If your tooling cannot append safely to #441, stop rather than creating a competing writer.

    Hosted RED is CI 35701602003 / rust job 106660704883: all 145 library tests passed, then five gateway mediation cases failed semantically. Implement the minimum Wardnet-owned causal boundary only: validate duplicate singleton/management headers and the 16,384-byte admitted request metadata budget before any upstream I/O, returning 400; request allowlist Content-Type, Accept, repeatable x-wardnet-app-meta, preserving multiplicity; strip Wardnet management credentials, Authorization/Cookie, proxy credentials, untrusted forwarding identity, attacker Host, framing/hop-by-hop fields, plus every case-insensitive Connection-nominated field; reconstruct only admitted upstream response metadata (Content-Type, repeatable x-wardnet-app-meta, Location, Retry-After, WWW-Authenticate) with multiplicity and the same dynamic Connection stripping. Ambiguous/over-budget admitted upstream singleton/metadata must fail as a bounded upstream error, not be silently downgraded.

    Do not absorb #442/#443 streaming/backpressure, #435 Coraza authority, EgressWeave transport authorization, quarantine isolation, contextual-orchestrator routing, appguardrail logic, or mutable sibling code. Keep DDD/API/test docs consistent if the production boundary introduces a named contract. Preserve exact-head evidence: run fmt, cargo test --locked --workspace including gateway_header_mediation, strict Clippy, and any owned coverage/rustdoc checks available on the branch. Do not claim GREEN from predecessor runs. Leave the PR Draft until exact-head hosted GREEN exists and the dependent stack is reconciled normally.

  4. seonghobae commented on Sep 23, 2026

    @seonghobae
    ContributorAuthor

    2026-09-23 KST exact-current serialized repair update for #441.

    #441 remains the sole production writer and is now exact a24065743b6697a91e16a1be54e62f8dc79d95d0 on exact Coraza parent #435 11e7fabfca76e0051fa883905228a0dbe472f4df. The earlier real loopback semantic RED remains valid. The current head is still a temporary one-shot synthesis-helper head: predecessor a63b540... acquired hosted compute in synthesis 35816872244 / job 107040157306, passed guard/synthesis/formatting, and failed in the local proof stage before any production-source commit. a240657... changes only that executed helper/proof-stage root cause (test-only legacy wrapper, unused generated test import, scratch formatting and explicit source scope); product mediation semantics and hostile assertions are unchanged.

    Current exact synthesis 35829979981, CI 35829985939, and Fuzz 35829986084 remain queued. They are incomplete evidence, not GREEN, and predecessor results do not transfer. The unchanged queue specimen has been handed to canonical runner owner .github#712; this Wardnet lane will not use wake/no-op commits, blind reruns, runner-selector changes, timeout inflation, central workflow copies, synthetic status or bypass.

    Two follow-on test-only gaps remain serialized behind the settled #441 source head rather than competing with it: #446 (ambiguous duplicate response Location/Retry-After) and #447/#448 (Content-Encoding representation integrity). Their latest hosted runs failed formatting/base checks before their hostile semantics executed, so neither currently authorizes additional production repair. #443 retains the independently proven streaming/backpressure semantic RED. Keep #440 open until the effective mediation code/test/docs reach protected main through normal governance.

  5. seonghobae commented on Sep 23, 2026

    @seonghobae
    ContributorAuthor

    Evidence correction / fresh exact-head status (2026-09-24 KST):

    Hosted #446 CI 35893328787, rust job 107290948038, on exact child 8ae44f90dec252140ad65075a96ad1c8b256f0ea passed formatting, all 151 library tests, the existing seven-case gateway mediation suite, and the repaired duplicate-Retry-After buyer-path case. The duplicate-Location case still returned 404, but the fixture used 302 Found; Wardnet's generic proxy uses ordinary reqwest redirect behavior, so reqwest followed the upstream Location before admit_response_headers() could inspect the original duplicated field set. The historical 404 is therefore not valid evidence that the repaired mediation boundary relayed duplicate Location.

    #446 exact b8b5804b655a424967252d8ac22172eb4a1338fb repairs only the hostile fixture to 201 Created with duplicate Location. That is a protocol-valid Location-bearing response that does not invoke redirect handling, so it isolates the Wardnet-owned header mediation boundary. Current exact CI 35918347719 is queued; do not claim current-child GREEN until it executes.

    The automatic-redirect observation is a distinct outbound-authority integration gap. Exact run/job/root-cause evidence and RED/GREEN acceptance were handed to canonical EgressWeave Rust-consumer issue ContextualWisdomLab/EgressWeave#237. Wardnet must not solve it by copying EgressWeave destination/DNS/redirect/proxy/TLS policy; EgressWeave's current GitHub Release inventory remains empty, so an immutable consumer bump is not yet available.

    Parent #441 remains the sole Wardnet production mediation writer at exact 29a35163fc5bab860f247a68a6b1d5f797d73182, with the minimum owned fix rejecting duplicate Content-Type, Location, and Retry-After. Keep #440 open until exact current source/test evidence reaches protected main through normal governance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions