Skip to content

feat(supervisor-middleware): add network egress middleware#2027

Open
pimlock wants to merge 54 commits into
mainfrom
1733-supervisor-middleware/pmlocek
Open

feat(supervisor-middleware): add network egress middleware#2027
pimlock wants to merge 54 commits into
mainfrom
1733-supervisor-middleware/pmlocek

Conversation

@pimlock

@pimlock pimlock commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the first usable RFC 0009 supervisor middleware slice: proto-backed, host-selected HTTP egress middleware for HttpRequest/pre_credentials, with both in-process built-ins and statically registered operator-run gRPC services.

The implementation covers RFC 0009 Phase 1 and adds basic external-service support from Phase 2. It establishes the contract, policy plumbing, ordered chain execution, built-in secret redaction, static gateway registration, relay integration, validation before policy persistence, body limits, audit events, and user-facing configuration and operations documentation.

Tip

See example middleware implementation: #2169
See high-level overview with interactive walk-through: https://gistpreview.github.io/?1f07f93cab1dab587d114fa6eec59c11/pr-2027-middleware-overview.html#request-lab

Related Issue

Closes #2010
Part of #1733
Design/RFC: #1738

Changes

  • Add the supervisor middleware contract and runtime, including the built-in openshell/secrets redactor and statically registered operator-run gRPC services.
  • Add ordered, host-selector-based network_middlewares policy configuration and validation, independent of the network policy rule that admits a request.
  • Enforce middleware on inspected HTTP requests before credential injection, with transformations, safe header additions, body limits, and explicit fail-open/fail-closed behavior.
  • Deliver effective service configuration to supervisors with resilient registry reload and last-known-good behavior.
  • Add OCSF observability plus gateway, policy, operations, architecture, and extensibility documentation.

Testing

  • mise run pre-commit passes
  • Unit and integration-style tests added or updated
  • E2E coverage considered
    • No separate gateway E2E was added. The request relay, policy validation, registry, and remote gRPC paths are covered by automated unit and integration-style tests.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated
  • Published docs updated

@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@pimlock pimlock force-pushed the 1733-supervisor-middleware/pmlocek branch 2 times, most recently from 595191e to 97b750f Compare June 26, 2026 23:58
@pimlock pimlock self-assigned this Jun 30, 2026
@pimlock pimlock force-pushed the 1733-supervisor-middleware/pmlocek branch from 358906a to 1fbcdbc Compare June 30, 2026 19:48
@github-actions

Copy link
Copy Markdown

@pimlock

This comment was marked as outdated.

@pimlock

This comment was marked as outdated.

2 similar comments
@pimlock

This comment was marked as outdated.

@pimlock

This comment was marked as outdated.

@pimlock

pimlock commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

/ok to test c4b0dcf

pimlock added 18 commits July 2, 2026 15:17
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
…are outages

An unreachable operator-registered middleware service previously aborted
sandbox startup via a hard error in load_policy, contradicting the
per-request on_error contract and the resilient live-reload path.

Retry the initial connect and, on failure, degrade to the built-in
registry so matched requests are governed by each config's on_error
(deny for fail_closed, allow for fail_open) instead of blocking the whole
sandbox. The policy poll loop now reconciles the registry on every poll
while an install is pending, so a recovered service is adopted without
waiting for a config change; a failed reconcile also no longer blocks
unrelated policy updates.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
…limit

A chain entry whose binding did not resolve reported a zero body limit,
which dragged the whole chain's buffer cap to zero and spuriously failed
body-bearing requests over capacity even when a resolved middleware could
have processed them. Exclude unresolved entries from the limit via a new
DescribedChainEntry::is_resolved(); when no entry resolves, skip buffering
and apply each entry's on_error directly.

Also fix two parallel-test flakes found while validating the change:

- Build middleware OCSF events into a Vec and assert on it directly
  instead of capturing through the global tracing pipeline, whose
  callsite-interest cache is process-global and raced under parallel runs.
- Accumulate the websocket deny response until the reason marker arrives
  rather than assuming a single read returns the full body.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock pimlock force-pushed the 1733-supervisor-middleware/pmlocek branch from c4b0dcf to 2b7cf4e Compare July 2, 2026 22:18
@pimlock

pimlock commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. I re-evaluated head be5e2fbb15596e3c0ad87850e0194ae21fa8cb2e after the fix(middleware): harden external service boundaries update.

What I checked: the complete current diff and the new typed mutation errors, request/response envelope budgets, finding and metadata caps, hop-by-hop filtering, OCSF emission bounds, tests, and Fern/proto documentation. The four findings from the prior public review are materially resolved: external mutation failures now use platform-owned codes, individual service responses are bounded, complete protobuf envelopes are budgeted, and the primary header/logging contracts now match the implementation.

Review findings:

  • P1 — Enforcement services can still use unauthenticated transport. crates/openshell-supervisor-middleware/src/lib.rs:347, crates/openshell-supervisor-middleware/src/remote.rs:28, and crates/openshell-supervisor-middleware/src/lib.rs:649: plaintext http:// is accepted, and each supervisor reconnects and trusts a fresh Describe response without cryptographically binding it to the service the gateway validated. A network attacker or workload able to occupy a shared/loopback endpoint could impersonate an enforcement service, receive request bodies, and return allow/mutation results. Require verified HTTPS for enforcement services; if plaintext must remain for development, gate it behind an explicit unsafe local-only mode that is not delivered to sandbox supervisors. Add plaintext and loopback rejection tests.
  • P1 — Policy and selected-chain cardinality remain unbounded. crates/openshell-policy/src/middleware.rs:184, crates/openshell-supervisor-middleware/src/lib.rs:696, and crates/openshell-supervisor-middleware/src/lib.rs:935: a policy near the 256 KiB policy cap can define many configs and selector patterns, then trigger sequential validation and request-path RPCs with a five-second timeout per stage. Add shared limits for configs, selected stages, and selector patterns; reject over-limit policies and defensively enforce the selected-chain cap at runtime, with max/max-plus-one tests.
  • P2 — Chunked credential rewriting still lacks an aggregate wire-size cap. crates/openshell-supervisor-network/src/l7/rest.rs:1044 and :1244: the credential-rewrite path passes None, leaving wire bytes unlimited while bounding only decoded bytes and individual lines. Many chunk extensions or trailers can consume unbounded supervisor I/O/CPU. Pass an aggregate wire cap and add multi-extension/trailer overflow tests.
  • P2 — Filtering Connection removes the data later needed to reject nominated-header mutations. crates/openshell-supervisor-network/src/l7/middleware.rs:287 filters both Connection and its nominated names, while crates/openshell-supervisor-middleware/src/headers.rs:123 rescans only that filtered list. A write/remove targeting a dynamically nominated header can therefore pass validation and be replayed against the original raw request. Carry the nominated-name set separately and add write/remove regressions such as Connection: x-openshell-middleware-tag.
  • P2 — Valid findings from later stages can be silently dropped. crates/openshell-supervisor-network/src/l7/middleware.rs:437 applies a 64-item cap to the whole chain outcome even though each stage may validly return 64. Bound the chain and enforce a derived chain-wide emission limit or the documented per-stage limit; add a multi-stage event-count test.
  • P3 — One Fern reference still advertises the obsolete transport headroom. docs/reference/gateway-config.mdx:166 says 4 MiB plus 64 KiB, while the implementation and docs/extensibility/supervisor-middleware.mdx:112 require 4 MiB plus 420 KiB. Update the gateway reference so operators do not configure a server that rejects platform-valid envelopes.

Docs: Fern navigation and the primary middleware page are present and substantially corrected; the gateway reference mismatch above remains.

Checks: required Branch Checks and test:e2e are still running for this head. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. I worked through the final review batch. The current head is 65ef2cdc.

Finding Disposition
Enforcement services can use unauthenticated transport Accepted limitation for this version. External middleware transport may remain unauthenticated, including plaintext HTTP where configured. We are okay with that limitation for this version and will address authenticated service transport separately.
Policy and selected-chain cardinality are unbounded Resolved in 65ef2cdc. Policies are limited to 10 middleware configs and 32 combined include/exclude selector patterns per config. Runtime selection independently enforces a maximum of 10 stages. Max and max-plus-one tests cover policy validation, validation callback suppression, chain description, evaluation, and OPA selection.
Chunked credential rewriting lacks an aggregate wire-size cap Resolved in 65ef2cdc. Credential rewriting now applies the 256 KiB aggregate cap to the complete chunked wire representation, including chunk framing, extensions, and trailers. Multi-extension and multi-trailer overflow regressions cover the aggregate boundary.
Connection filtering loses nominated-header protection data Resolved in 65ef2cdc. Connection-nominated names are captured before filtering and carried separately from middleware-visible headers. Header mutation validation rejects both writes and removals targeting those names, with regressions for x-openshell-middleware-tag.
Valid findings from later stages can be silently dropped Resolved in 65ef2cdc. Each stage is capped at 32 findings, and a 10-stage chain retains and emits the derived maximum of 320 findings. Overflow remains an invalid stage response governed by on_error, and multi-stage retention plus OCSF event-count tests cover the boundary.
Gateway reference advertises obsolete transport headroom Resolved in 65ef2cdc. The bounded response components now derive 292 KiB of protobuf envelope headroom, and the gateway, middleware, architecture, policy, and proto documentation use the same limits.

Validation completed:

  • mise run pre-commit passes at 65ef2cdc, including formatting, Markdown, license, Python, Helm, workspace checks, strict Clippy with -D warnings, and the full Rust test suite.
  • All 125 policy tests and 50 supervisor-middleware tests pass.
  • All 931 supervisor-network tests pass, with 5 LocalStack-dependent tests ignored as expected.
  • Focused regressions cover aggregate chunked-wire overflow, Connection-nominated write/remove rejection, maximum policy and chain cardinality, complete 320-finding retention, bounded OCSF emission, and the exact 292 KiB envelope budget.

Please re-review the current head when ready.

@pimlock

pimlock commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. I re-evaluated head 65ef2cdc2a28f212e6781aedda28aaadf4a380bf after your July 14 update describing how you addressed the final review batch.

What I checked: the complete current diff and the new policy/chain cardinality limits, aggregate chunked-wire cap, Connection-nominated header protection, 320-finding chain retention, protobuf envelope budget, tests, and Fern/proto documentation. Those prior findings are materially resolved. I also respected your maintainer scope direction accepting unauthenticated external middleware transport as a limitation for this version and did not repeat it as a finding.

Review findings:

  • P1 — The built-in secret redactor leaks suffixes of quoted secrets containing whitespace, commas, or escaped quotes. crates/openshell-supervisor-middleware-builtins/src/secrets.rs:71: the value regex stops at whitespace or comma. For example, "password":"correct horse battery staple" becomes "password":"[REDACTED] horse battery staple" while the middleware returns ALLOW, so fail_closed cannot catch the leak (CWE-200). Use parser-aware JSON redaction plus a robust quoted-value scanner for text formats, and add coverage for spaces, commas, escapes, and multiline values.
  • P2 — Chunked request trailers are silently discarded when middleware buffering normalizes the body. crates/openshell-supervisor-network/src/l7/rest.rs:1281 consumes every trailer, while :979 converts the request to Content-Length without removing the original Trailer declaration. Merely selecting middleware therefore changes request semantics even if no stage transforms the body, and integrity trailers such as Digest disappear. Preserve validated trailers when re-emitting the request or explicitly reject trailer-bearing middleware requests; add an end-to-end framing regression.
  • P2 — Policy validation rejects the documented fail_open behavior for tls: skip. crates/openshell-policy/src/middleware.rs:273 rejects every selector overlapping a tls: skip endpoint regardless of on_error, making the all-fail_open bypass path at crates/openshell-supervisor-network/src/proxy.rs:1250 unreachable. Restrict the validation conflict to effective fail_closed configs and test acceptance of fail_open while retaining default/fail-closed rejection.

Docs: Fern docs and navigation are present; the fail_open/tls: skip contract needs to agree with validation.

Checks: required Branch Checks and test:e2e were still pending when this review began. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings in commit 24fd2292:

  • Renamed the example built-in from openshell/secrets to openshell/regex. The implementation and docs now describe it as a best-effort, regex-based text transformation with no guarantee that sensitive values will be detected or fully removed. It currently uses only fixed built-in expressions; configurable expressions are left as explicit follow-up work.
  • Fixed chunked request normalization in the REST middleware and body credential-rewrite paths. These buffered paths already consumed and discarded actual trailers; they now also remove the stale Trailer declaration when replacing chunked framing with Content-Length. Transparent, non-buffered chunk relay continues to preserve framing and trailers.
  • Confirmed that JSON-RPC/MCP, GraphQL, and inference already consistently discard trailers when buffering and regenerating requests. Added regression coverage for both REST buffering paths.
  • Updated policy validation so middleware overlapping a tls: skip endpoint is accepted when on_error: fail_open, while the default and explicit fail_closed modes remain rejected because inspection cannot be guaranteed.

I also filed #2284 to consolidate the duplicated buffered HTTP framing and normalization logic behind a shared core with async and slice-based adapters.

mise run pre-commit passes, including workspace checks/tests and server integration tests.

pimlock added 2 commits July 14, 2026 17:47
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the follow-up review. I worked through this batch of findings. The current head is ba5200e7.

Finding Disposition
Service-advertised timeout can exceed the operator deadline Resolved in ba5200e7. A binding timeout may shorten the operator-configured deadline but cannot extend it. Tests cover longer advertised timeouts for both ValidateConfig and EvaluateHttpRequest.
Built-in redactor leaks partial values and over-redacts suffix keys Resolved in ba5200e7. The broad keyword matcher was removed from the defaults. The built-in now keeps only the simple bounded sk- token expression, with adversarial coverage for whitespace, commas, escaped quotes, unquoted assignments, and suffix-key false positives.
Malformed or continued headers can bypass middleware inspection Resolved in ba5200e7. Shared strict request-header validation now rejects folded continuations, missing colons, whitespace before colons, and invalid field-name tokens before policy evaluation, middleware projection, or forwarding. The proxy did not previously provide equivalent validation; this closes that gap rather than relying on downstream parsing.
Chunked trailers are silently discarded during REST transformations Resolved for the two new REST transformation paths in ba5200e7. Requests with non-empty trailers are rejected before middleware normalization or request-body credential rewriting. Existing GraphQL and JSON-RPC trailer behavior predates this PR and is documented for follow-up in #2284.
Unauthenticated middleware transport Acknowledged limitation for this version. We are comfortable shipping the current unauthenticated transport and will handle transport authentication separately.

Validation completed:

  • mise run pre-commit passes.
  • Focused timeout tests cover both middleware RPCs.
  • Focused malformed-header tests prove invalid requests are rejected instead of forwarded.
  • Trailer rejection tests cover both middleware and credential-rewrite transformation paths.

Please re-review the current head when ready.

@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. I re-evaluated head ba5200e773ecf4198db6eba9cc0c3313b3d18998 after your July 15 update covering timeout ceilings, the narrowed regex built-in, strict header parsing, chunked-trailer handling, and the accepted unauthenticated-transport limitation.

What I checked: the complete current diff and the affected CONNECT, forward-proxy, REST, policy-composition, gateway configuration, test, and Fern documentation paths. The timeout, regex scope, trailer rejection, cardinality, finding-limit, and documentation fixes you described are present. I also confirmed your repository-admin scope direction and did not repeat unauthenticated middleware transport as a finding.

Review findings:

  • P1 — Connection-nominated headers can bypass middleware inspection. crates/openshell-supervisor-network/src/l7/middleware.rs:317 removes headers named by Connection from the middleware projection, but crates/openshell-supervisor-network/src/l7/rest.rs:1024 retains them in rebuilt requests; the forward proxy likewise replaces Connection while copying its nominated header upstream at crates/openshell-supervisor-network/src/proxy.rs:3280. An input such as Connection: x-guard therefore hides X-Guard from middleware while still delivering it upstream (CWE-444). Strip dynamically nominated headers from forwarded bytes with a deliberate WebSocket exception, and add CONNECT plus forward-proxy regressions.
  • P1 — Strict header validation does not validate field values or all forward requests. crates/openshell-supervisor-network/src/l7/rest.rs:305 validates field names but permits bare CR, NUL, DEL, and other invalid controls in values. Direct forward requests are initially parsed lossily at crates/openshell-supervisor-network/src/proxy.rs:641, while strict validation occurs only when a middleware chain is selected at :4555. Validate the raw byte-level header block once at ingress, including allowed field-value bytes, before CONNECT/forward dispatch. Add matching- and nonmatching-host regressions proving malformed inputs neither invoke middleware nor reach upstream (CWE-444).
  • P2 — Unsupported transfer codings are silently misclassified or normalized. crates/openshell-supervisor-network/src/l7/rest.rs:2194 accepts any value containing chunked and treats other transfer codings as no body; buffered middleware then removes the full Transfer-Encoding field at :1024. Reject every unsupported sequence (including invalid order/duplicates) or fully decode and preserve remaining codings. Cover gzip, gzip, chunked, chunked, gzip, duplicate chunked, and CL/TE combinations (CWE-444).
  • P2 — Provider composition can create an effective policy that gateway admission never validates. Provider rules are composed at crates/openshell-server/src/grpc/policy.rs:1236, followed only by middleware binding registration checks at :1249. A composed tls: skip endpoint can overlap a source policy's fail-closed selector and be rejected later by the supervisor. Structurally validate the composed policy before delivery and test this overlap.
  • P2 — Helm cannot render external middleware registrations. deploy/helm/openshell/templates/gateway-config.yaml and deploy/helm/openshell/values.yaml expose no middleware registration or raw TOML extension, so the external-service slice is unavailable through the supported Kubernetes installation path. Add typed Helm values, render [[openshell.supervisor.middleware]], and test multiple registrations and optional timeouts.

Docs: Fern docs and navigation are present and cover the middleware contract; the Helm configuration surface above is still missing.

Checks: Branch Checks, DCO, and Helm are green. test:e2e is applied and E2E remains in progress. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-check. I addressed the four runtime and policy findings in 19043041 and moved the Helm packaging surface to a focused follow-up.

Finding Disposition
Connection-nominated headers can bypass middleware inspection Resolved in 19043041. CONNECT/REST relay and direct forward-proxy paths now remove every field nominated by Connection before upstream forwarding. A validated WebSocket handshake preserves only the required Connection: Upgrade and Upgrade: websocket pair. Regressions cover both forwarding paths and the WebSocket exception.
Strict header validation does not validate field values or all forward requests Resolved in 19043041. Raw request headers are validated once at proxy ingress before CONNECT or forward dispatch, independent of host, policy, or middleware selection. Invalid UTF-8, folded lines, bare CR/LF, NUL, DEL, other invalid controls, malformed names, invalid Connection options, and framing/routing fields nominated by Connection are rejected before middleware invocation or upstream connection. Matching- and nonmatching-host regressions receive 400 at ingress.
Unsupported transfer codings are silently misclassified or normalized Resolved in 19043041. Request framing supports exactly one Transfer-Encoding: chunked value. Unsupported, layered, reordered, empty, or duplicate codings are rejected, and every Content-Length plus Transfer-Encoding combination fails closed. Tests cover gzip, gzip, chunked, chunked, gzip, duplicate chunked, multiple TE fields, and CL/TE combinations.
Provider composition can create an effective policy that admission never validates Resolved in 19043041. The gateway reruns structural safety validation after provider composition and before hashing or delivering the effective policy. A provider-derived tls: skip endpoint overlapping source-policy fail-closed middleware now returns FAILED_PRECONDITION; regression coverage exercises that cross-layer conflict.
Helm cannot render external middleware registrations Deferred to #2286. This is a Kubernetes packaging and typed configuration-surface gap, not a runtime security-boundary defect. Static registration through [[openshell.supervisor.middleware]] in gateway TOML works in this PR. The follow-up specifies typed Helm values, multiple registrations, optional timeout rendering, documentation, and chart tests.

Validation completed:

  • mise run pre-commit passes, including workspace formatting, strict Clippy, compilation, and tests.
  • The complete affected network and server crate suites pass.
  • Focused regressions cover CONNECT and forward-proxy header stripping, malformed matching and nonmatching requests, transfer-coding variants, WebSocket preservation, and provider-composed policy rejection.

Please re-review the current head 19043041 when ready.

@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. I re-evaluated head 19043041c0d1ffe46fe9039afb6f55c64d8709df after your July 15 response describing the Connection-header, ingress-validation, transfer-coding, and provider-composition fixes.

What I checked: the complete current diff and the CONNECT/REST relay, direct forward-proxy, buffered middleware, provider-composition, audit, test, and Fern documentation paths. The four runtime and policy findings from the prior review are materially resolved. I also respected your earlier note that the pre-existing pipelined-request boundary issue is tracked in #2251 and did not count it as a finding here, and I treated Helm rendering as the focused follow-up in #2286.

Review findings:

  • P1 — The WebSocket exception preserves arbitrary additional Upgrade values. crates/openshell-supervisor-network/src/l7/rest.rs:1936, :1663, and crates/openshell-supervisor-network/src/proxy.rs:3306: a request is classified as WebSocket when any Upgrade value contains websocket, after which every original Upgrade field is preserved. Upgrade: websocket plus Upgrade: h2c can therefore reach upstream, contrary to the intended “only the validated WebSocket pair” invariant. Strip all original Connection and Upgrade fields and append exactly Connection: Upgrade and Upgrade: websocket; add duplicate-field and mixed comma-separated protocol regressions for both relay paths.
  • P2 — Buffered middleware requests can retain Expect: 100-continue. crates/openshell-supervisor-network/src/l7/rest.rs:984, :1021, and :1040: acknowledge_expect_continue removes Expect, but it is called only when no body bytes arrived with the headers. With partial read-ahead, the proxy can wait for remaining body bytes without sending 100 Continue; with a complete read-ahead body, it forwards Expect alongside the completed body and can treat an upstream 100 Continue as the final response. Always remove Expect after locally buffering, send the local 100 whenever additional client bytes must be read, and add partial/complete fixed and chunked read-ahead tests.
  • P2 — Forward success is recorded before middleware and credential processing can deny the request. crates/openshell-supervisor-network/src/proxy.rs:4534 emits FORWARD allowed and allowed activity before middleware runs, while :4615 can subsequently record a middleware denial. This yields contradictory audit events and double-counts a request that never reached upstream. Emit final success only after middleware, token grant, credential rewrite, and generation checks succeed, or label the early event explicitly as an intermediate policy decision; add a middleware-denial assertion that no final forward-success activity is recorded.

Docs: Fern documentation and docs/index.yml navigation are present; no new documentation blocker remains.

Checks: Branch Checks and Helm Lint are green; required test:e2e is still pending. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. Per maintainer request, I ran a fresh same-SHA gpt-5.5 code-only review of head 19043041c0d1ffe46fe9039afb6f55c64d8709df after your July 15 response describing the Connection-header, ingress-validation, transfer-coding, and provider-composition fixes.

What I checked: the current forward-proxy, CONNECT/REST relay, buffered middleware, provider-composition, audit, test, and Fern documentation paths. The prior Connection-nominated header stripping, unsupported transfer-coding rejection, provider-composed policy validation, middleware config validation, and tls: skip conflict concerns look addressed. I also respected your note that the pipelined request-boundary issue is tracked separately in #2251, and treated Helm rendering as the focused follow-up in #2286.

Review findings:

  • P2 - Forward success is still recorded before later request gates can deny the request. crates/openshell-supervisor-network/src/proxy.rs:4534 emits FORWARD allowed OCSF and success activity before middleware, token grant, credential injection, and the final generation guard can still deny at crates/openshell-supervisor-network/src/proxy.rs:4617 and crates/openshell-supervisor-network/src/proxy.rs:4694. This can create false allow records for requests that never reach upstream. Move that event and emit_forward_success_activity(...) after the final relay succeeds, or rename the early event as policy admission and keep it out of request-success accounting. Add a regression where middleware denial or token failure produces no forward-success activity.
  • P2 - Buffered middleware can still forward a stale Expect: 100-continue when body bytes arrive with the headers. crates/openshell-supervisor-network/src/l7/rest.rs:991 strips Expect only when no body bytes were already read. The forward proxy can read headers plus body overflow in one socket read, so a fully buffered request can be rebuilt and forwarded with Expect even though OpenShell has already consumed and rewritten the body. Split "send 100" from "strip Expect": always remove Expect after local body buffering, and only send 100 Continue when no body bytes have been consumed and more body bytes must be read. Add fixed and chunked read-ahead regressions for this path.

Docs: Fern documentation and docs/index.yml navigation are present; no new documentation blocker remains.

Checks: Branch Checks, DCO, Helm Lint, and required test:e2e are green. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-check. I addressed all three findings in 9923194a.

Finding Disposition
WebSocket exception preserves arbitrary additional Upgrade values Resolved in 9923194a. Both CONNECT/REST relay and direct forward-proxy paths now strip every original Connection and Upgrade field after classifying the request, then append exactly Connection: Upgrade and Upgrade: websocket. Duplicate-field and mixed comma-separated h2c, websocket regressions cover both forwarding paths.
Buffered middleware requests can retain Expect: 100-continue Resolved in 9923194a. Once middleware takes responsibility for buffering, the proxy always consumes Expect locally. It sends 100 Continue whenever fixed-length or chunked framing still requires client bytes, and does not send an unnecessary interim response when the complete body arrived as read-ahead. Partial and complete fixed-length and chunked regressions verify the local acknowledgement and that Expect is never forwarded with the normalized body.
Forward success is recorded before middleware and credential processing can deny Resolved in 9923194a. Final OCSF allowed and activity records now occur only after middleware, token grant, credential rewriting, policy-generation checks, and the HTTP relay succeed. Middleware denial retains only its denial activity; regression coverage asserts that no final success activity is emitted.

Validation completed:

  • mise run pre-commit passes, including workspace formatting, strict Clippy, compilation, and tests.
  • All 946 openshell-supervisor-network unit tests pass.
  • Focused regressions cover both WebSocket forwarding paths, partial and complete fixed/chunked read-ahead, and middleware-denial activity.

Please re-review the current head 9923194a when ready.

drew
drew previously approved these changes Jul 15, 2026
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Blocked

Gator is blocked by merge conflicts against main.

Head SHA: 9923194aa71d5ccba8281e5cf2666444ca0130b2

GitHub currently reports this PR as not mergeable (mergeable_state: dirty). Required checks, including E2E, are green and maintainer approval is present, but gator cannot advance this head while the branch has conflicts.

Next action: @pimlock needs to rebase or merge main into the PR branch and resolve the conflicts, then push the updated head for re-check.

@pimlock pimlock added gator:blocked Gator is blocked by process or repository gates and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Jul 15, 2026
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
@pimlock

pimlock commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

gator-agent

Re-check After Author Update

Thanks @pimlock. I re-evaluated head a12f91bdfea84dc06b7098db7b0821bcafce22a9 after the merge-main/conflict-resolution update.

What I checked: the current PR diff against main, with focus on the forward-proxy, CONNECT/REST relay, middleware buffering, policy admission, server config delivery, audit, tests, and Fern documentation paths. The previous merge-conflict blocker is resolved (mergeable: true). The prior findings around connection-nominated headers, WebSocket canonicalization, Expect: 100-continue, final success auditing, body/envelope bounds, finding cardinality, docs, and the explicit follow-ups for unauthenticated middleware transport and Helm rendering were not repeated.

Review findings:

  • P1 - Malformed HTTP request lines can pass policy and middleware inspection, then reach upstream. crates/openshell-supervisor-network/src/l7/rest.rs:311, crates/openshell-supervisor-network/src/proxy.rs:680, and crates/openshell-supervisor-network/src/proxy.rs:3274: header-field validation does not validate the request line, the forward proxy only takes the first two whitespace tokens, and rewrite can preserve malformed version text upstream. A request such as GET http://api.example/p HTTP/1.1 extra can be admitted and rewritten as GET /p HTTP/1.1 extra, creating an HTTP parser differential after middleware inspection (CWE-444). Add shared raw request-line validation before CONNECT/forward dispatch and buffered middleware construction: require exactly METHOD SP target SP HTTP/1.0|HTTP/1.1, validate the method as an HTTP token, reject controls in the target, and add matching/nonmatching-host regressions proving malformed request lines neither invoke middleware nor reach upstream.
  • P2 - Unsupported forward-proxy schemes are forwarded as HTTP while middleware sees the original scheme. crates/openshell-supervisor-network/src/proxy.rs:3165, :3216, :3528, and :4571: parse_proxy_uri accepts any URI scheme, defaults non-https schemes to port 80, rejects only https, then passes the original scheme into middleware. ftp://api.example/p can therefore be forwarded as plain HTTP while external middleware receives scheme = "ftp". Reject every forward-proxy scheme except http before policy or middleware evaluation, and add an unsupported-scheme regression.

Docs: Fern docs and docs/index.yml navigation are present for the direct UX surface.

Checks: Branch Checks and required test:e2e are currently pending for this head. No local tests were run as part of this code-only review.

Next state: gator:in-review pending an author update for these findings.

@pimlock pimlock added gator:in-review Gator is reviewing or awaiting PR review feedback gator:blocked Gator is blocked by process or repository gates and removed gator:blocked Gator is blocked by process or repository gates gator:in-review Gator is reviewing or awaiting PR review feedback labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gator:blocked Gator is blocked by process or repository gates test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement RFC 0009 phase 1 supervisor middleware

5 participants