Skip to content

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

Open
TonyLuo-NV wants to merge 1 commit 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 1 commit 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gator:in-review Gator is reviewing or awaiting PR review feedback

Projects

None yet

2 participants