Skip to content

fix(network): fail closed when credential placeholders cannot be rewritten#2162

Open
TonyLuo-NV wants to merge 6 commits into
NVIDIA:mainfrom
TonyLuo-NV:fix/fail-closed-credential-placeholder
Open

fix(network): fail closed when credential placeholders cannot be rewritten#2162
TonyLuo-NV wants to merge 6 commits into
NVIDIA:mainfrom
TonyLuo-NV:fix/fail-closed-credential-placeholder

Conversation

@TonyLuo-NV

@TonyLuo-NV TonyLuo-NV commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Makes the sandbox egress proxy fail closed when it cannot rewrite a credential placeholder, instead of forwarding the literal openshell:resolve:env:<NAME> token (or its provider-alias form) to the upstream. Closes two fail-open paths that leak the reserved placeholder on the wire and cause upstream auth failures when internal state is degraded.

Related Issue

Fixes #2161[DGX Spark] gateway never rewrites openshell:resolve:env: credential placeholder on sandbox egress — literal placeholder sent on the wire.

Full chain for reviewers:

The invariant enforced here: a live credential placeholder must never leave the proxy unresolved. Degraded internal state fails closed; explicit policy opt-outs (tls: skip) keep working.

Changes

Leak G3 — resolver-None fail-open (crates/openshell-core/src/secrets.rs)

rewrite_http_header_block took an early return that forwarded the header block verbatim whenever resolver was None. The existing fail-closed marker scan runs only after that early return (on the resolved path), so a header block carrying a live placeholder was passed through untouched. It now scans the header region for the reserved markers (openshell:resolve:env: prefix and the OPENSHELL-RESOLVE-ENV- alias marker, raw and percent-decoded) even when there is no resolver, and returns UnresolvedPlaceholderError { location: "header" } if one is present. The scan window mirrors the resolved-path scan (header_end + 256). Marker-free traffic still passes through unchanged, so credential-free endpoints are unaffected.

Leak G2 — tls_state == None raw-tunnel fallback (crates/openshell-supervisor-network/src/proxy.rs)

When TLS was detected on a CONNECT but tls_state was None (ephemeral CA generation or CA file write failed at startup, see run.rs), handle_tcp_connection emitted a Low-severity event and fell back to a raw copy_bidirectional tunnel — silently bypassing credential rewrite. Inside the proxy handler tls_state is None only on CA-init failure: the mode != Proxy case never starts this handler, and tls: skip is handled by an earlier early return. The handler now refuses the connection with a 503 Service Unavailable JSON error and a High-severity Denied/Blocked denial event instead of tunneling. The explicit tls: skip opt-out and all other modes are untouched.

Severity escalation (crates/openshell-supervisor-network/src/run.rs)

The two startup CA-failure logs ("Failed to write CA files…" and "Failed to generate ephemeral CA…") are raised from Medium to High, since a disabled TLS termination now fails closed on TLS-bearing connections.

Testing

All run locally with the sandbox HTTP proxy unset (the sandbox HTTP_PROXY=127.0.0.1:8118 intercepts loopback connections and makes the inference-routing socket tests fail spuriously; they pass with it unset and are unrelated to this change).

  • cargo test -p openshell-core secrets::58 passed, 0 failed. New unit tests: placeholder in request line / header value / provider-alias form with resolver=None all fail closed; the pre-existing marker-free resolver=None passthrough test still passes.
  • cargo test -p openshell-supervisor-network812 lib tests + integration passed, 0 failed. The relay integration test (relay_request_without_resolver_fails_closed_on_placeholder) now asserts the request is rejected before any byte reaches upstream; a new test_json_error_response_503_tls_termination_unavailable locks the G2 fail-closed response contract.
  • cargo fmt --all -- --check → clean.
  • cargo clippy -p openshell-core -p openshell-supervisor-network --all-targets -- -D warnings → clean.

A true connection-level test of the G2 branch was not added: handle_tcp_connection requires a real TcpStream with a peeked TLS ClientHello and no existing test drives it, so the observable fail-closed response contract is locked via the build_json_error_response test instead.

Two pre-existing tests that documented the vulnerable behavior were updated to assert the fail-closed outcome:

  • secrets.rs: no_resolver_passes_through_without_scanning → split into request-line / header-value / alias-marker fail-closed tests.

  • l7/rest.rs: relay_request_without_resolver_leaks_placeholdersrelay_request_without_resolver_fails_closed_on_placeholder.

  • Unit tests added/updated

  • E2E tests added/updated (if applicable)

Checklist

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

Signed-off-by: Tony Luo xialuo@nvidia.com

🤖 Generated with Claude Code

…itten

When the credential rewriter degrades internally, the proxy forwarded the
literal `openshell:resolve:env:<NAME>` placeholder (or its provider alias
marker) to the upstream instead of the resolved secret, leaking the reserved
token on the wire and causing upstream auth failures (NVIDIA#2161).

Two fail-open paths are closed:

- secrets: `rewrite_http_header_block` returned the header block verbatim when
  no `SecretResolver` was available, so the fail-closed marker scan (which ran
  only on the resolved path) never saw the placeholder. It now scans the
  header region for reserved markers even with no resolver and returns
  `UnresolvedPlaceholderError` when one is present. Marker-free traffic still
  passes through unchanged.

- proxy: when TLS was detected on a CONNECT but `tls_state` was `None`
  (ephemeral CA generation or CA file write failed at startup), the handler
  fell back to a raw `copy_bidirectional` tunnel, bypassing credential
  rewrite. Inside the proxy handler `tls_state` is `None` only on CA-init
  failure (`mode != Proxy` never starts the handler, and `tls: skip` is
  handled earlier), so it now refuses the connection with a 503 and a
  High-severity denial event instead of tunneling. The two startup CA-failure
  logs are raised from Medium to High.

Tests: resolver=None with a placeholder in the request line, a header value,
and the provider-alias form now fail closed; marker-free passthrough is
unchanged; the relay integration test asserts the request is rejected before
any byte reaches upstream; and the 503 fail-closed response contract is
locked.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 7, 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.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

All contributors have signed the DCO ✍️ ✅
Posted by the DCO Assistant Lite bot.

@TonyLuo-NV

Copy link
Copy Markdown
Author

I have read the DCO document and I hereby sign the DCO.

@TonyLuo-NV TonyLuo-NV marked this pull request as ready for review July 7, 2026 07:00
cv added a commit to NVIDIA/NemoClaw that referenced this pull request Jul 7, 2026
#6379) (#6382)

<!-- markdownlint-disable MD041 -->
## Summary

`mcp status` could report all-green provider metadata while OpenShell
never rewrote the `openshell:resolve:env:` credential placeholder on
egress, so every agent request failed with the literal placeholder as
the bearer token and the managed MCP server was silently skipped. This
PR adds a wire-level credential-resolution probe to `mcp status` and
`mcp add` so that failure mode is loud and attributable.

## Related Issue

Fixes the NemoClaw-side diagnostic gap of #6379 (keep that issue open
until the upstream fix lands).

Full chain for reviewers:

- **Bug report (NemoClaw)**: #6379 — managed MCP unusable at runtime,
`mcp status` stays all-green
- **This PR (NemoClaw, diagnostic surface)**: wire-level differential
credential-resolution probe in `mcp status` / `mcp add`
- **Root-cause issue (upstream)**: NVIDIA/OpenShell#2161 — gateway never
rewrites the `openshell:resolve:env:` placeholder on egress
- **Root-cause fix (upstream)**: NVIDIA/OpenShell#2162 — fail closed
when credential placeholders cannot be rewritten

## Changes

- New `src/lib/actions/sandbox/mcp-bridge-resolution-probe.ts`: builds
an in-sandbox MCP `initialize` probe (curl wrapped in the adapter
runtime so the generated `protocol: mcp` policy attributes it to the
adapter binary ancestry, same construction as the live E2E DNS-rebinding
probe), classifies the outcome, and renders the operator warning. All
output is redacted via `redactBridgeSecretsForDisplay`.
- `statusMcpBridge` gains a `probeCredentialResolution` option; the
verdict is surfaced as `provider.credentialResolution` (`ok: true |
false | null`, `httpStatus`, `detail`) plus a warning naming the
OpenShell host defect on failure. The probe shares the existing
legacy-credential skip gate.
- `mcp status <server>` probes by default; bare `mcp status` and `mcp
list` never probe; `--probe` / `--no-probe` override both directions and
combining them is rejected.
- `mcp add` runs the probe once after the durable add commits and prints
a loud warning on failure without failing the add (exit code stays 0;
`--no-probe` skips).
- Classification never blames the credential rewrite for endpoint
problems: HTTP 2xx = resolved, 400/401/403 = resolution failure,
404/5xx/CONNECT-403/timeout/unreachable = indeterminate with detail.
- Human-readable status renders a `credential resolution: verified /
FAILED / unknown` line; help text documents the new flags.
- Docs: `docs/deployment/set-up-mcp-bridge.mdx` (operate +
troubleshooting) and `docs/reference/commands.mdx` (+ synced
`commands-nemohermes.mdx`).

## Type of Change

- [x] Code change with doc updates

## Quality Gates

- [x] Tests added or updated for changed behavior
- [x] Docs updated for user-facing behavior changes
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [ ] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: requesting maintainer
review on this PR

## Verification

- [x] PR description includes the DCO sign-off declaration and every
commit appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run check:diff` passed when hooks were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — command/result: `npx vitest run
--project cli
src/lib/actions/sandbox/mcp-bridge-resolution-probe.test.ts
src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts` → 22/22
passed; full `src/lib/actions/sandbox/` CLI lane → 1291 passed (3
unrelated pre-existing local-env failures: two 5s-timeout flakes that
pass standalone, one host-python missing `yaml`); `npx vitest run
--project integration test/mcp-add-crash-consistency.test.ts
test/mcp-bridge-servers.test.ts test/mcp-openshell-workflow.test.ts
test/mcp-provider-ownership.test.ts
test/mcp-artifact-secret-scan.test.ts` → 41/41 passed; `npm run
typecheck:cli` clean
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [x] `npm run docs` builds without warnings (doc changes only)

Signed-off-by: Tony Luo <xialuo@nvidia.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added optional wire-level credential-resolution probing for MCP
`status` (runs by default when exactly one server is named).
* Added `--probe` / `--no-probe` support for `add` and `status`,
including a post-`add` verification that warns on failure without
failing the command.
* `status` now can display a “credential resolution” line with HTTP
indicators.
* **Bug Fixes**
* Improved verdict semantics and safer probing/skipping rules (including
“unknown” vs “FAILED” outcomes).
* **Tests**
* Added coverage for probe generation, classification, and CLI flag
behavior.
* **Documentation**
* Updated `mcp`, Hermes/NemoClaw, and troubleshooting docs to reflect
the new probe reporting and guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Tony Luo <xialuo@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Co-authored-by: Carlos Villela <cvillela@nvidia.com>
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

PR Review Status

Validation: this PR is project-valid because it is a concentrated security hardening fix for linked issue #2161, which documents a reproducible sandbox egress credential-placeholder leak path.
Head SHA: 1c567f94a73301139c6306d822f9f09688ebcf56

Review findings:

  • crates/openshell-supervisor-network/src/proxy.rs:1039 sends HTTP/1.1 200 Connection Established before TLS detection, but the new failure branch writes the intended 503 later at crates/openshell-supervisor-network/src/proxy.rs:1247. For normal CONNECT clients, the first observable response is still success; the JSON 503 is sent inside the tunnel and will likely surface as a TLS handshake/protocol error instead of the documented tls_termination_unavailable response. It also logs an allowed CONNECT before the denial. Please make CA-init failure a pre-200 Connection Established decision for non-tls: skip CONNECTs, and add a connection-level test that asserts the first bytes returned to the client are the 503 when tls_state is unavailable.

Docs: no Fern docs update is required for this code-only degraded-state hardening.

Next state: gator:in-review

@johntmyers johntmyers added the gator:in-review Gator is reviewing or awaiting PR review feedback label Jul 9, 2026
…ilable

The fail-closed refusal for a terminating CONNECT with no TLS termination
state (ephemeral CA init failed) was written after the 200 Connection
Established response. Because a CONNECT client only sends its TLS
ClientHello after reading the 200, the peek-based TLS detection is
inherently post-200, so the 503 landed inside the established tunnel and
surfaced to the client as a TLS protocol error rather than a readable
status. An 'allowed CONNECT' event was also logged first.

Move the decision to a pre-200 gate: query_tls_mode resolves purely from
the policy decision + host/port (no peeked bytes), so the route's TLS
treatment is known before the tunnel is acknowledged. When TLS state is
absent and the route is not tls: skip, write the 503 as the first bytes
on the socket, emit the High-severity Denied event, and close. tls: skip
routes tunnel raw exactly as before, and no allowed-CONNECT event is
emitted on the refusal path.

The now-unreachable post-200 branch is kept as defense in depth but no
longer writes an in-tunnel 503; it fails closed by dropping the
connection instead.

Add connection-level regression tests over a real loopback socket: the
gate refuses with HTTP/1.1 503 as the first bytes for a terminating
route, and writes nothing when TLS termination is present or the route is
tls: skip.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@TonyLuo-NV

Copy link
Copy Markdown
Author

Re: @johntmyers gator-agent finding on the post-200 in-tunnel 503

Thanks — this was a real flaw. Fixed in e736ee9c. Because the peek-based TLS detection is inherently post-200 (the client only sends its ClientHello after reading the 200), the refusal had to move earlier, before the tunnel is acknowledged.

What changed (pre-200 decision):

  • The CA-init-failure refusal is now a gate evaluated before HTTP/1.1 200 Connection Established, right after the OPA allow/deny decision (proxy.rs:756, the 200 for this path is later at proxy.rs:1086). The lookup used is query_tls_mode(&opa_engine, &decision, &host_lc, port), which resolves purely from the policy decision + host/port with no peeked bytes, so the route's TLS treatment (tls: skip vs. terminate) is known pre-200.
  • When tls_state is None (ephemeral CA failed at startup) and the route is not tls: skip, the new helper refuse_connect_when_tls_unavailable (proxy.rs:4445) writes the 503 tls_termination_unavailable JSON as the first bytes on the socket and returns true; the handler then emits the High-severity Denied/Blocked OCSF event + denial notification and closes. A normal CONNECT client now reads the documented 503 instead of a TLS protocol error.
  • tls: skip routes are exempt and tunnel raw exactly as before.
  • No "allowed CONNECT" event precedes the denial — that log lives after the 200 (now unreachable for the refused path), so the refusal emits only the Denied event.

Defense-in-depth choice: the old post-200 in-tunnel refusal branch (proxy.rs:1255) is now unreachable in normal operation (the pre-200 gate handles it, and tls: skip raw-tunnels before the peek). I kept it as belt-and-suspenders in case a future refactor bypasses the gate, but it no longer writes an in-tunnel 503 (which was the reported bug). It fails closed by dropping the connection instead of raw-tunneling — dropping avoids both the credential leak and corrupting the client's already-established TLS handshake with meaningless HTTP bytes.

New connection-level tests (proxy.rs, real loopback TcpStream pair):

  • connect_without_tls_termination_refused_with_503_before_200 — terminating route, tls_state unavailable: asserts the first bytes the client reads back start with HTTP/1.1 503 Service Unavailable (and do not contain 200 Connection Established), with body error = tls_termination_unavailable.
  • connect_with_tls_termination_or_skip_proceeds_without_writing — asserts the gate writes nothing (client reads clean EOF) when TLS termination is present or the route is tls: skip.

Validation (run with sandbox proxy env unset):

env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo fmt --all -- --check          # clean
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
  cargo clippy -p openshell-supervisor-network --all-targets -- -D warnings                        # clean
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo test -p openshell-supervisor-network
  # lib: 814 passed; 0 failed  (811 prior + 3 new)
  # integration: system_inference 3 passed, websocket_upgrade 2 passed, sigv4 5 ignored (need LocalStack)
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo test -p openshell-core          # 299 passed; 0 failed

No Fern docs change (code-only degraded-state hardening), matching your docs note.

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @TonyLuo-NV. I re-evaluated latest head e736ee9cc5d0fe7ef60cd6ab6e949594f35c76a4 after your update about moving the TLS-unavailable refusal before 200 Connection Established.

What I checked: crates/openshell-supervisor-network/src/proxy.rs, crates/openshell-core/src/secrets.rs, crates/openshell-supervisor-network/src/l7/rest.rs, and crates/openshell-supervisor-network/src/run.rs, including the new pre-200 gate, the tls: skip path, the resolver-None header scan, and the new regression tests.

Disposition: partially resolved. The original post-200 in-tunnel 503 issue is addressed in the implementation shape, and no Fern docs update is required for this code-only degraded-state hardening.

Remaining items:

  • crates/openshell-supervisor-network/src/proxy.rs:756 adds the pre-200 gate, but the new connection-level regression at crates/openshell-supervisor-network/src/proxy.rs:7915 calls only refuse_connect_when_tls_unavailable, not handle_tcp_connection. Please add a handler-level test that sends an actual CONNECT api.example.com:443 request through handle_tcp_connection with tls_state = None, asserts the first bytes are HTTP/1.1 503 Service Unavailable and never 200 Connection Established, and also covers a real tls: skip policy path through the handler.
  • crates/openshell-supervisor-network/src/proxy.rs:756 currently runs the TLS-unavailable refusal before the SSRF and allowed_ips validation that starts at crates/openshell-supervisor-network/src/proxy.rs:793. During CA-init failure, an internal-address CONNECT that would normally produce 403 ssrf_denied and SSRF telemetry now returns 503 tls_termination_unavailable first. This is still fail-closed, but it weakens operator visibility for blocked internal probes during degraded state. Please keep computing effective_tls_skip early, but perform the TLS-unavailable refusal after SSRF validation and before HTTP/1.1 200 Connection Established.
  • In crates/openshell-supervisor-network/src/l7/rest.rs:4915, the test says nothing should reach upstream but only checks that captured bytes do not contain the placeholder or secret. Please make the assertion match the invariant with forwarded.is_empty().

Next state: gator:in-review

Addresses the gator re-check on NVIDIA#2162.

Ordering: the pre-200 fail-closed refusal ran before SSRF/allowed_ips
validation, so during CA-init failure an internal-address CONNECT got a
503 tls_termination_unavailable instead of the normal 403 ssrf_denied,
weakening operator visibility in degraded state. The SSRF branches now
return validated addresses; the refusal runs after that validation (an
internal address has already been denied with 403) but still before the
upstream connect and before 200 Connection Established. effective_tls_skip
is still resolved up front since the refusal consumes it.

Tests: add connection-level regressions through the real
handle_tcp_connection, driving a CONNECT from a child /bin/bash copy so
the /proc process-identity binding resolves it against a permissive
policy (the hot-swap test's identity pattern). They assert the first
bytes are HTTP/1.1 503 for a terminating route with no TLS state, a 403
(not 503) for an internal address, and no refusal for a tls: skip route.
These are gated to Linux at runtime (evaluate_opa_tcp needs /proc); a
companion test verifies the OPA policy shape (glob allow, tls mode) on
every platform so the precondition is locked where /proc is unavailable.

rest.rs: tighten the fail-closed relay test to assert the forwarded
buffer is_empty() rather than merely lacking the placeholder/secret.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@TonyLuo-NV

Copy link
Copy Markdown
Author

Re: @johntmyers gator re-check (partially resolved)

All three remaining items are addressed in 0148b415.

1. Ordering — refusal now runs after SSRF/allowed_ips validation. The SSRF branches no longer connect inline; they return the validated SocketAddr list (let validated_addrs = if …, proxy.rs:774). The TLS-unavailable refusal runs after that match (proxy.rs:1035) — so an internal-address CONNECT has already been denied with 403 ssrf_denied + SSRF telemetry — but before the upstream connect (proxy.rs:1077) and before 200 Connection Established. effective_tls_skip is still resolved up front (query_tls_mode, no peeked bytes) since the gate consumes it. A new handler-level test locks this: an internal-address CONNECT with tls_state = None yields the 403, not the 503.

2. Handler-level regression through handle_tcp_connection. New tests drive a real CONNECT through the full entry point (not just the extracted helper), asserting the first bytes on the socket:

  • connect_handler_refuses_terminating_route_with_503_before_200 (proxy.rs:8077) — terminating route, tls_state = None: first bytes are HTTP/1.1 503 Service Unavailable, response never contains 200 Connection Established.
  • connect_handler_returns_ssrf_403_for_internal_address_not_503 (proxy.rs:8116) — internal address 127.0.0.1:443: HTTP/1.1 403 Forbidden / ssrf_denied, never 503 (proves item 1's ordering end-to-end).
  • connect_handler_tls_skip_route_is_not_refused (proxy.rs:8154) — a real tls: skip policy path: the handler proceeds past the gate to the tunnel connect and writes no refusal/denial.

Approach and why it's shaped this way: evaluate_opa_tcp binds process identity via /proc, and on non-Linux it denies unconditionally, so a real handler-level test can't be fully portable. These tests use the identity pattern already proven by the resolve_process_identity_..._hot_swap regression — a child /bin/bash copy opens the connection via /dev/tcp so /proc resolves it against a permissive glob policy — and are gated to Linux at runtime (CI runners are Linux; they no-op with a skip message on other platforms, so they still compile there). The 503 and 403 cases short-circuit before the upstream connect, so they never egress (the terminating target is RFC 5737 TEST-NET-3, which passes SSRF but is never actually dialed). To lock the OPA precondition where /proc isn't available, a companion test handler_test_policy_allows_glob_binary_and_reads_tls_mode (proxy.rs:8191) runs on every platform and verifies the policy shape (glob binary → Allow with matched policy; tls: skipTlsMode::Skip; terminating → TlsMode::Auto). I did not add a bypass flag to production; handle_tcp_connection is unchanged apart from the ordering fix.

Note: I develop on macOS, so the three Linux-gated handler tests execute on CI, not locally. The policy-shape test and the connection-level helper tests below run and pass locally.

3. rest.rs fail-closed assertion. relay_request_without_resolver_fails_closed_on_placeholder now asserts forwarded.is_empty() (verified it holds) instead of merely checking the placeholder/secret are absent — the stronger invariant the test name claims.

Validation (sandbox proxy env unset):

env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo fmt --all -- --check          # clean
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
  cargo clippy -p openshell-supervisor-network --all-targets -- -D warnings                        # clean
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo test -p openshell-supervisor-network
  # lib: 818 passed; 0 failed  (+4: three Linux-gated handler tests + the portable policy-shape test)
  # integration: system_inference 3, websocket_upgrade 2 passed; sigv4 5 ignored (need LocalStack)
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy cargo test -p openshell-core          # 299 passed; 0 failed

No Fern docs change (code-only degraded-state hardening), matching your note.

@johntmyers johntmyers added gator:watch-pipeline Gator is monitoring PR CI/CD status test:e2e Requires end-to-end coverage and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Jul 10, 2026
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @TonyLuo-NV. I re-evaluated latest head 0148b4154178870124ca10dde9245aad777fe0f2 after your update that addressed the remaining handler-level CONNECT coverage, SSRF-before-TLS-unavailable ordering, and the stricter REST relay invariant.

What I checked: crates/openshell-supervisor-network/src/proxy.rs, crates/openshell-supervisor-network/src/l7/rest.rs, crates/openshell-core/src/secrets.rs, and crates/openshell-supervisor-network/src/run.rs, including the full handle_tcp_connection gate ordering, the tls: skip path, SSRF telemetry preservation, resolver-None fail-closed scanning, and the new regression tests. I also ran the required independent code-only reviewer for this head SHA.

Disposition: resolved. The previous gator findings are addressed, and the independent reviewer found no blocking correctness, security, regression, or maintainability findings remaining.

Remaining items:

  • No blocking review items remain.

Docs: no Fern docs update is required for this code-only degraded-state hardening.

E2E: I applied test:e2e because this changes network proxy behavior and provider credential fail-closed handling.

Next state: gator:watch-pipeline

@github-actions

Copy link
Copy Markdown

Label test:e2e applied for 0148b41. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute the standard E2E suite after building the required gateway and supervisor images once. The matching required CI gate status on this PR will flip green automatically once the run finishes.

@johntmyers johntmyers added gator:in-review Gator is reviewing or awaiting PR review feedback and removed gator:watch-pipeline Gator is monitoring PR CI/CD status labels Jul 10, 2026
The handler regression tests forked cat to read the proxy reply, so the
client socket fd was inherited by a second process with a different
binary. The identity resolver correctly denies that as ambiguous
shared-socket ownership (the same invariant
resolve_process_identity_denies_fork_exec_shared_socket_ambiguity pins),
so the tests exercised the deny path instead of the allow path — and on
busy CI runners the deny-path /proc fallback scan exceeded the test
budget and looked like a hang. The client script now uses only bash
builtins (exec, printf, read -d '') so exactly one process owns the
socket, and the child is left to exit on EOF instead of being killed
mid-read.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@TonyLuo-NV

Copy link
Copy Markdown
Author

CI failure on the two handler tests root-caused and fixed in f694fc0 — it was a test-client bug, not a product bug:

The test's bash client forked cat to read the proxy reply, so the client socket fd was inherited by a second process with a different binary. The identity resolver correctly denies that as ambiguous shared-socket ownership (the same invariant resolve_process_identity_denies_fork_exec_shared_socket_ambiguity pins), so the handler took the deny path instead of reaching the refusal gate. On busy CI runners the deny path's /proc fallback scan exceeded the test budget, which surfaced as "hang"; the underlying decision was the same deny.

Fix: the client script now uses only bash builtins (exec 3<>/dev/tcp/..., printf, IFS= read -r -d '') so exactly one process ever owns the socket, and the child exits on EOF instead of being killed mid-read.

Verified in a Linux (aarch64) container with rust:1.95.0:

  • cargo test -p openshell-supervisor-network --lib connect_handler_ → 3 passed / 0 failed (503-before-200, SSRF-403-not-503, tls-skip-not-refused)
  • full cargo test -p openshell-supervisor-network --lib839 passed / 0 failed
  • cargo fmt --all -- --check and cargo clippy -p openshell-supervisor-network --all-targets -- -D warnings → clean

The product-side ordering fix from the previous commit is unchanged.

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @TonyLuo-NV. I re-evaluated latest head f694fc00b3ed116a9a513242ccf29067275bd827 after your update that the handler test client was made fork-free.

What I checked: crates/openshell-supervisor-network/src/proxy.rs, crates/openshell-supervisor-network/src/procfs.rs, crates/openshell-core/src/secrets.rs, and the current Branch Checks result. I also ran the required independent code-only reviewer for this head SHA.

Disposition: not resolved. The product-side fail-closed ordering still looks correct, but the current head's OpenShell / Branch Checks run still fails on Rust (linux-arm64-cpu8) in the two new handler tests.

Remaining items:

  • crates/openshell-supervisor-network/src/proxy.rs:8048 sets the handler test entrypoint_pid to the test harness PID. That can miss the spawned bash client in the descendant fast path and fall back to scanning all of /proc in crates/openshell-supervisor-network/src/procfs.rs:365, which matches the current CI timeouts at proxy.rs:8103 and proxy.rs:8140. Please use the spawned bash client's PID as the entrypoint_pid in drive_connect_through_handler, so the identity resolver stays on the intended descendant path.
  • crates/openshell-core/src/secrets.rs:1906 covers raw no-resolver placeholder markers, but not the percent-decoded marker path claimed by the PR. Please add a no-resolver regression with a %3A-encoded openshell:resolve:env:KEY marker in the request target or header.

Docs: no Fern docs update is required for this code-only degraded-state hardening.

E2E: test:e2e remains appropriate because this changes network proxy behavior and provider credential fail-closed handling. E2E is still pending for this head.

Next state: gator:in-review

The child-process client (even fork-free) made the handler tests
environment-sensitive: on CI runners with a busy or restricted /proc,
resolving the child's socket ownership degraded into the whole-/proc
fallback scan and a deny, which surfaced as a hang. The client is now an
in-process TcpStream and the test policy allows current_exe(), so
identity resolution binds the socket to the test process itself in the
descendant scan — the same in-process pattern the passing
resolve_process_identity tests rely on.

The tls: skip test additionally asserts that the handler emitted no
DenialEvent at any stage, so it can no longer pass vacuously on a
policy or identity deny. Refusal budgets widened to 30s as a belt for
slow runners; the refusals themselves return in milliseconds.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@TonyLuo-NV

Copy link
Copy Markdown
Author

The remaining handler-test flake is fixed in cc8aaf4 by removing the child process entirely:

Why the previous two attempts still failed on CI: any child-process client (even fork-free bash) makes the test depend on resolving the child's socket ownership through /proc. On CI runners with a busy or permission-restricted /proc, that resolution degrades into the whole-/proc fallback scan and a deny — which surfaces as the "hang" panic. The same tests pass in a quiet container, which is why it was invisible locally.

Redesign: the client is now an in-process TcpStream and the test policy allows current_exe(), so identity resolution binds the socket to the test process itself via the descendant scan — the same in-process pattern the consistently-passing resolve_process_identity_* tests rely on, with no environment-sensitive fallback path.

Non-vacuous tls: skip: the harness now threads a denial_tx and the tls: skip test asserts the handler emitted no DenialEvent at any stage, so it can no longer pass vacuously on a policy or identity deny.

Verified in a Linux (aarch64) rust:1.95.0 container, both with an isolated pid namespace and with --pid=host (busy-/proc simulation): the 3 handler tests pass in both, and the full cargo test -p openshell-supervisor-network --lib suite is 839 passed / 0 failed. cargo fmt --check and clippy -D warnings clean.

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @TonyLuo-NV. I re-evaluated latest head cc8aaf42397adf4e02f9dd6ef6dee74a9f34f02d after your update that the CONNECT handler tests now use an in-process TcpStream/current_exe() client and assert the tls: skip path emits no denial event.

What I checked: crates/openshell-supervisor-network/src/proxy.rs, crates/openshell-supervisor-network/src/l7/rest.rs, crates/openshell-core/src/secrets.rs, and the current Branch Checks/E2E status. I also ran the required independent code-only reviewer for this head SHA.

Disposition: partially resolved. The product-side fail-closed path looks correct now: SSRF/allowed_ips validation happens before the TLS-unavailable 503 gate, 200 Connection Established stays after that gate, and the tls: skip path remains exempt. The REST relay invariant also now checks that no bytes reach upstream. No Fern docs update is required for this code-only degraded-state hardening.

Remaining items:

  • crates/openshell-core/src/secrets.rs:1906 still lacks the requested no-resolver regression for a percent-encoded canonical marker. The implementation should catch this through contains_reserved_credential_marker, but please add a test such as GET /api/openshell%3Aresolve%3Aenv%3AKEY HTTP/1.1 with resolver = None and assert it fails closed with UnresolvedPlaceholderError { location: "header" }.

E2E: test:e2e remains appropriate because this changes network proxy behavior and provider credential fail-closed handling. E2E is still pending for this head.

Next state: gator:in-review

The no-resolver scan already catches the percent-encoded canonical
marker through its decoded pass; this regression pins it: a request
line carrying openshell%3Aresolve%3Aenv%3AKEY with no resolver must
fail closed with UnresolvedPlaceholderError { location: header }.

Signed-off-by: Tony Luo <xialuo@nvidia.com>
@TonyLuo-NV

Copy link
Copy Markdown
Author

@johntmyers The remaining item is addressed in cb5bc51: added no_resolver_with_percent_encoded_marker_fails_closedGET /api/openshell%3Aresolve%3Aenv%3AKEY HTTP/1.1 with resolver = None fails closed with UnresolvedPlaceholderError { location: "header" } via the percent-decoded pass of contains_reserved_credential_marker, exactly as the implementation intends. cargo test -p openshell-core secrets:: → 59 passed / 0 failed (Linux container, rust:1.95.0); cargo fmt --check clean.

E2E: understood that test:e2e stays applied — the label-triggered run needs a maintainer Re-run all jobs on the existing workflow per the bot instructions whenever you're ready.

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @TonyLuo-NV. I re-evaluated latest head cb5bc5156bd7518f4930bb5612b1f97075f49a8b after your update that added the percent-encoded no-resolver placeholder regression.

What I checked: crates/openshell-core/src/secrets.rs, crates/openshell-supervisor-network/src/proxy.rs, crates/openshell-supervisor-network/src/l7/rest.rs, and crates/openshell-supervisor-network/src/run.rs, including the no-resolver raw and percent-decoded marker scan, the pre-200 TLS-unavailable CONNECT refusal, SSRF-before-503 ordering, the tls: skip path, and the updated regression tests. I also ran the required independent code-only reviewer for this head SHA.

Disposition: resolved. The previous gator finding is addressed, and the independent reviewer found no blocking correctness, security, regression, maintainability, or missing-test issues remaining.

Remaining items:

  • No blocking review items remain.

Docs: no Fern docs update is required for this code-only degraded-state hardening.

E2E: test:e2e remains appropriate because this changes network proxy behavior and provider credential fail-closed handling. Branch Checks and Helm Lint are green; OpenShell / E2E is still pending.

Next state: gator:watch-pipeline

@johntmyers johntmyers added gator:watch-pipeline Gator is monitoring PR CI/CD status and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Jul 10, 2026
@TonyLuo-NV

Copy link
Copy Markdown
Author

E2E status on cb5bc51: Branch Checks are green (the redesigned in-process handler tests passed on both Linux runners), and the only failure is e2e / E2E (rust-podman-rootless, ubuntu-26.04) on upload_respects_gitignore_by_default (tests/sync.rs) — sandbox download fails with ssh tar create exited with status 1, i.e. the file-sync path over SSH.

That surface is disjoint from this PR: the diff touches the HTTP CONNECT proxy path (SSRF→TLS-gate ordering) and the credential-placeholder header rewrite, neither of which is on the sandbox upload/download SSH tar path, and the fail-closed branches only trigger on CA-init failure or reserved markers — neither occurs in that test. The other three E2E matrix legs (docker, podman root, GPU) passed.

@johntmyers could you re-run the failed E2E job when you get a chance? (Contributors can't re-run workflows on this repo.)

@TonyLuo-NV

Copy link
Copy Markdown
Author

Additional evidence that the rust-podman-rootless sync failure is a flake: this same PR's Branch E2E Checks ran green twice earlier today — run 29072053859 (05:44Z) and run 29067340520 (03:40Z) — on head 0148b41, whose product code is identical to the current head (the only commits since, cc8aaf4 and cb5bc51, are test-only changes to proxy.rs's test module and one secrets.rs unit test). The failing upload_respects_gitignore_by_default sync test passed in both of those runs on the same code path. A re-run of the failed job should clear it.

@johntmyers johntmyers added gator:approval-needed Gator completed review; maintainer approval needed and removed gator:watch-pipeline Gator is monitoring PR CI/CD status labels Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gator:approval-needed Gator completed review; maintainer approval needed test:e2e Requires end-to-end coverage

Projects

None yet

2 participants