fix(network): fail closed when credential placeholders cannot be rewritten#2162
Open
TonyLuo-NV wants to merge 1 commit into
Open
fix(network): fail closed when credential placeholders cannot be rewritten#2162TonyLuo-NV wants to merge 1 commit into
TonyLuo-NV wants to merge 1 commit into
Conversation
…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>
|
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. |
|
All contributors have signed the DCO ✍️ ✅ |
Author
|
I have read the DCO document and I hereby sign the DCO. |
This was referenced Jul 7, 2026
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
mcp statusstayed all-greenmcp status/mcp addThe 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-
Nonefail-open (crates/openshell-core/src/secrets.rs)rewrite_http_header_blocktook an early return that forwarded the header block verbatim wheneverresolverwasNone. 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 theOPENSHELL-RESOLVE-ENV-alias marker, raw and percent-decoded) even when there is no resolver, and returnsUnresolvedPlaceholderError { 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 == Noneraw-tunnel fallback (crates/openshell-supervisor-network/src/proxy.rs)When TLS was detected on a CONNECT but
tls_statewasNone(ephemeral CA generation or CA file write failed at startup, seerun.rs),handle_tcp_connectionemitted a Low-severity event and fell back to a rawcopy_bidirectionaltunnel — silently bypassing credential rewrite. Inside the proxy handlertls_stateisNoneonly on CA-init failure: themode != Proxycase never starts this handler, andtls: skipis handled by an earlier early return. The handler now refuses the connection with a503 Service UnavailableJSON error and a High-severityDenied/Blockeddenial event instead of tunneling. The explicittls: skipopt-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:8118intercepts 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 withresolver=Noneall fail closed; the pre-existing marker-freeresolver=Nonepassthrough test still passes.cargo test -p openshell-supervisor-network→ 812 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 newtest_json_error_response_503_tls_termination_unavailablelocks 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_connectionrequires a realTcpStreamwith a peeked TLS ClientHello and no existing test drives it, so the observable fail-closed response contract is locked via thebuild_json_error_responsetest 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_placeholders→relay_request_without_resolver_fails_closed_on_placeholder.Unit tests added/updated
E2E tests added/updated (if applicable)
Checklist
Signed-off-by: Tony Luo xialuo@nvidia.com
🤖 Generated with Claude Code