Skip to content

fix(infra): resolve ssh client from trusted system dirs (#83289)#93030

Open
manan-tech wants to merge 1 commit into
openclaw:mainfrom
manan-tech:fix-ssh-resolve-system-bin
Open

fix(infra): resolve ssh client from trusted system dirs (#83289)#93030
manan-tech wants to merge 1 commit into
openclaw:mainfrom
manan-tech:fix-ssh-resolve-system-bin

Conversation

@manan-tech

@manan-tech manan-tech commented Jun 14, 2026

Copy link
Copy Markdown

Summary

resolveSshConfig and startSshPortForward spawned a hard-coded /usr/bin/ssh. On hosts where the system ssh client lives elsewhere, both SSH config discovery and gateway tunneling fail, notably:

  • Windows: the built-in OpenSSH client is at %SystemRoot%\System32\OpenSSH\ssh.exe, never /usr/bin/ssh.
  • NixOS: ssh resolves under /run/current-system/sw/bin, not /usr/bin.

This routes both helpers through the repo's existing trusted-binary resolver, resolveSystemBin("ssh", { trust: "strict" }), and adds the Windows built-in OpenSSH directory to the resolver's trusted Windows locations (it already trusted System32 but not the System32\OpenSSH subdirectory where ssh.exe actually ships).

Closes #83289.

Why this shape

  • Reuses the canonical resolveSystemBin (the same resolver used for openssl/ffmpeg) instead of adding a new path-resolution helper.
  • The tunnel fails closed with a clear diagnostic (surfaced through the existing sshTunnelError path) when no system ssh client is present; the best-effort config reader returns null, consistent with its other failure branches.
  • Because resolveSystemBin only returns paths that exist and are executable, startSshPortForward can no longer reach spawn with a non-existent binary.

Trust-boundary note

The only new trusted location is %SystemRoot%\System32\OpenSSH, the OS-managed home of the Windows built-in OpenSSH client. It is added under strict trust (OS-managed directories only); no user-writable or PATH-derived directory is trusted, preserving the trusted-subprocess boundary for a security-sensitive binary. Git-for-Windows / winget ssh under Program Files is intentionally not covered by strict.

Live Windows verification

Reproduced on a real Windows Server 2022 Datacenter host (built-in OpenSSH client, OpenSSH_for_Windows_9.5p2). The old hard-coded path is absent, and the real client resolves exactly to the directory this PR adds to strict trust:

# The old hard-coded path does not exist on Windows (this IS the bug):
Test-Path C:\usr\bin\ssh    ->  False
Test-Path /usr/bin/ssh      ->  False

# The real client lives exactly where this PR adds to strict trust:
Test-Path C:\Windows\System32\OpenSSH\ssh.exe   ->  True
(Get-Command ssh).Source                        ->  C:\Windows\System32\OpenSSH\ssh.exe
ssh -V                                           ->  OpenSSH_for_Windows_9.5p2, LibreSSL 3.8.2

So on a real Windows host the pre-fix code spawns a path that does not exist, while resolveSystemBin("ssh", { trust: "strict" }) (with the System32\OpenSSH entry this PR adds) resolves the genuine OS client.

Real behavior proof

  • Behavior addressed: SSH config discovery and gateway SSH tunneling resolve the platform ssh client (including the Windows built-in OpenSSH) instead of a hard-coded /usr/bin/ssh.
  • Real environment tested: macOS (arm64) and a live Windows Server 2022 host (built-in OpenSSH). Resolver branch logic also covered by platform/filesystem/spawn-mocked unit tests.
  • Exact steps or command run after this patch:
    • macOS: node scripts/run-vitest.mjs src/infra/resolve-system-bin.test.ts src/infra/ssh-config.test.ts src/infra/ssh-tunnel.test.ts src/commands/gateway-status.test.ts; pnpm tsgo:core and pnpm tsgo:core:test; node scripts/run-oxlint.mjs and oxfmt --check on the changed files.
    • Windows: Test-Path of the old/new paths, Get-Command ssh, ssh -V (output above).
  • Evidence after fix: 61 targeted tests pass on macOS. A new test asserts resolveSystemBin("ssh") resolves ...\System32\OpenSSH\ssh.exe under a mocked Windows environment; helper tests assert the resolved path is what gets spawned and that the tunnel fails closed when none is found. Reverting only the resolver change makes the Windows test fail with expected [...] to include 'System32\OpenSSH'. On the live Windows host, Get-Command ssh resolves to C:\Windows\System32\OpenSSH\ssh.exe while /usr/bin/ssh does not exist.
  • Observed result after fix: on macOS, resolveSystemBin("ssh", { trust: "strict" }) returns /usr/bin/ssh, identical to the removed literal (no Unix regression); on real Windows Server 2022 it resolves C:\Windows\System32\OpenSSH\ssh.exe.
  • What was not tested: a live end-to-end gateway tunnel handshake against a remote gateway over SSH; 32-bit Node on 64-bit Windows (System32 file-system redirection / Sysnative), which the resolver does not special-case for any binary.

resolveSshConfig and startSshPortForward spawned a hard-coded /usr/bin/ssh,
so SSH config discovery and gateway tunneling failed wherever the system ssh
client lives elsewhere: Windows (built-in OpenSSH under System32\OpenSSH) and
NixOS (/run/current-system/sw/bin).

Route both helpers through the existing resolveSystemBin("ssh", { trust:
"strict" }) resolver, and add the Windows built-in OpenSSH directory to the
resolver's trusted Windows locations (it had System32 but not the
System32\OpenSSH subdirectory where ssh.exe actually ships). Fail closed with
a clear diagnostic when no system ssh client is present.

Closes openclaw#83289.
@openclaw-barnacle openclaw-barnacle Bot added size: S proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 14, 2026
@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 10:42 PM ET / 02:42 UTC.

Summary
The PR routes SSH config discovery and SSH tunneling through resolveSystemBin("ssh", { trust: "strict" }) and adds Windows System32\OpenSSH to the strict trusted directory list.

PR surface: Source +18, Tests +63. Total +81 across 6 files.

Reproducibility: yes. Source inspection of current main and v2026.6.11 shows both SSH helper paths still spawn /usr/bin/ssh, which fails on hosts where the trusted SSH client lives in a platform-specific system directory.

Review metrics: 1 noteworthy metric.

  • Strict Windows trusted directories: 1 added. The added System32\OpenSSH directory changes the shared strict subprocess lookup boundary, which maintainers should notice before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #83289
Summary: This PR is the active candidate fix for the canonical hard-coded SSH executable bug report.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦐 gold shrimp
Patch quality: 🐚 platinum hermit
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted Windows or NixOS output from patched OpenClaw resolving ssh through resolveSystemBin or exercising the SSH config/tunnel helper path.
  • Rebase on current main while preserving the IPv4 loopback and PortInUseError SSH tunnel hardening.
  • Get maintainer acceptance for treating System32\OpenSSH as a strict trusted Windows subprocess directory.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR includes copied Windows terminal output proving the OS OpenSSH location, but it still does not show patched OpenClaw resolving or spawning that path after the fix. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] The branch is currently conflicting and must be rebased without losing current-main SSH tunnel hardening around IPv4 loopback binding and PortInUseError handling.
  • [P1] The supplied Windows proof shows the OS OpenSSH location but not patched OpenClaw resolving or spawning that executable after the fix.
  • [P1] The diff widens the shared strict Windows trusted subprocess directory list, so maintainers need to explicitly accept System32\OpenSSH as part of that security boundary.

Maintainer options:

  1. Require patched proof and trust acceptance (recommended)
    Ask for redacted target-platform output from patched OpenClaw resolving or using ssh, then have maintainers explicitly accept System32\OpenSSH as a strict trusted Windows directory before merge.
  2. Accept source plus OS-path proof
    Maintainers can intentionally accept the mocked resolver coverage and copied Windows terminal output if they are comfortable with the remaining real-behavior proof gap.
  3. Request an SSH-specific trust shape
    If System32\OpenSSH should not become part of the global strict resolver list, pause this branch and ask for an SSH-only trusted-resolution contract instead.

Next step before merge

  • [P1] The remaining action is maintainer review for the security boundary plus contributor real behavior proof and rebase, not a narrow automated code repair.

Security
Needs attention: The diff does not show a concrete exploit, but it widens a shared strict trusted subprocess directory boundary that needs maintainer security acceptance.

Review details

Best possible solution:

Rebase the trusted-resolver fix onto current main, preserve the existing SSH tunnel hardening, add redacted patched OpenClaw proof on Windows or NixOS, and get maintainer acceptance for the strict Windows trust-boundary change.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection of current main and v2026.6.11 shows both SSH helper paths still spawn /usr/bin/ssh, which fails on hosts where the trusted SSH client lives in a platform-specific system directory.

Is this the best way to solve the issue?

Yes for the code direction: reusing resolveSystemBin("ssh", { trust: "strict" }) is narrower than PATH lookup or duplicated platform branches. Merge readiness still depends on a clean rebase, patched target-platform proof, and maintainer acceptance of the trust-boundary expansion.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4ac5cf863673.

Label changes

Label changes:

  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.

Label justifications:

  • P2: This is a normal-priority cross-platform SSH helper bug with limited code surface and real remote gateway impact.
  • merge-risk: 🚨 security-boundary: The PR widens the shared strict Windows subprocess search path used for trusted binary resolution.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR includes copied Windows terminal output proving the OS OpenSSH location, but it still does not show patched OpenClaw resolving or spawning that path after the fix. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +18, Tests +63. Total +81 across 6 files.

View PR surface stats
Area Files Added Removed Net
Source 3 20 2 +18
Tests 3 66 3 +63
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 6 86 5 +81

Security concerns:

  • [medium] Accept the new strict Windows trusted directory — src/infra/resolve-system-bin.ts:79
    Adding System32\OpenSSH affects the shared strict resolver, so maintainers should explicitly decide that this OS-managed subdirectory is acceptable for strict subprocess lookup.
    Confidence: 0.82

What I checked:

  • Repository policy applied: Root AGENTS.md was read fully; no scoped AGENTS.md directly owns src/infra/**, so the deep PR review, proof, and security-boundary guidance applies. (AGENTS.md:1, 4ac5cf863673)
  • Current main still hard-codes SSH config lookup: resolveSshConfig still sets sshPath to /usr/bin/ssh before spawning ssh -G, so Windows OpenSSH under System32\OpenSSH is not reached on current main. (src/infra/ssh-config.ts:79, 4ac5cf863673)
  • Current main still hard-codes SSH tunnel spawn: startSshPortForward still passes /usr/bin/ssh directly to spawn, so the tunnel path has the same platform-specific executable-resolution failure. (src/infra/ssh-tunnel.ts:165, 4ac5cf863673)
  • Existing trusted resolver contract: resolveSystemBin is the shared resolver for internal infrastructure binaries and explicitly avoids PATH-controlled lookup for spawn/execFile callers. (src/infra/resolve-system-bin.ts:155, 4ac5cf863673)
  • Gateway status reaches the affected tunnel helper: The gateway status probe dynamically loads startSshPortForward, so the hard-coded executable affects the user-facing SSH gateway status path. (src/commands/gateway-status/probe-run.ts:70, 4ac5cf863673)
  • PR head uses the trusted resolver for SSH config: At the PR head, resolveSshConfig resolves ssh through resolveSystemBin("ssh", { trust: "strict" }) and returns null without spawning when none is found. (src/infra/ssh-config.ts:82, 97f40195c6be)

Likely related people:

  • steipete: GitHub commit history shows this account authored the commits that introduced the SSH tunnel path and later the SSH config helper where the hard-coded executable behavior lives. (role: feature-history owner; confidence: high; commits: d258c68ca1ab, 1ec1f6dcbf95; files: src/infra/ssh-tunnel.ts, src/infra/ssh-config.ts, src/commands/gateway-status.ts)
  • optimol: The trusted system-binary resolver and Windows install-root handling that this PR extends were introduced in the non-user-writable paths work. (role: introduced resolver trust model; confidence: high; commits: c40884d306e8; files: src/infra/resolve-system-bin.ts, src/infra/windows-install-roots.ts, src/infra/resolve-system-bin.test.ts)
  • vincentkoc: Current checkout blame for the affected helper and resolver lines points at a recent release-sync commit by this account, making them relevant for current-main rebase preservation even though the original behavior predates that commit. (role: recent adjacent contributor; confidence: low; commits: 3e2646a7864c; files: src/infra/ssh-config.ts, src/infra/ssh-tunnel.ts, src/infra/resolve-system-bin.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-06-21T19:53:49.285Z sha 97f4019 :: needs real behavior proof before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jun 14, 2026
@manan-tech

Copy link
Copy Markdown
Author

Added live Windows Server 2022 verification to the PR description. On a real Windows host:

  • the old hard-coded /usr/bin/ssh (and C:\usr\bin\ssh) does not exist — the source-level failure, confirmed live;
  • (Get-Command ssh).Source resolves to C:\Windows\System32\OpenSSH\ssh.exe, exactly the directory this PR adds to strict trust;
  • ssh -V confirms the client runs (OpenSSH_for_Windows_9.5p2, LibreSSL 3.8.2).

This is the after-fix behavior on a host where /usr/bin/ssh fails. The System32\OpenSSH addition stays under strict trust (OS-managed only, no PATH or user-writable directories).

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 6, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: S stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SSH helpers hard-code a Unix-only ssh path

1 participant