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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant