Skip to content

feat: per-server SSH agent forwarding (ForwardAgent)#53

Merged
bvisible merged 1 commit into
mainfrom
feat/forward-agent
Jul 13, 2026
Merged

feat: per-server SSH agent forwarding (ForwardAgent)#53
bvisible merged 1 commit into
mainfrom
feat/forward-agent

Conversation

@bvisible

Copy link
Copy Markdown
Owner

Summary

Implements per-server SSH agent forwarding (the equivalent of OpenSSH's ForwardAgent yes), as requested in #52 by @raphaelbahat. Processes on the remote host can authenticate to other SSH hosts using the keys in the operator's local ssh-agent — e.g. git clone over SSH on a remote server using a local GitHub key — without copying any private key to the server.

Thanks @raphaelbahat for the exceptionally well-researched issue (accurate code citations, correct security note, right implementation location).

Changes

  • src/config-loader.js — read the new option from both formats into a camelCase forwardAgent field, and emit it from exportToToml():
    • .env: SSH_SERVER_<NAME>_FORWARD_AGENT=true
    • TOML: forward_agent = true
    • A new parseBool() helper coerces the value. This matters: in .env every value is a string, so a naive read would make FORWARD_AGENT=false truthy. parseBool treats only true/1/yes/on as true; false/absent/empty → false.
  • src/ssh-manager.js — set connConfig.agentForward = true only inside the existing if (process.env.SSH_AUTH_SOCK) block. ssh2 throws ("You must set a valid agent path to allow agent forwarding") if agentForward is set without a valid agent, so nesting it guarantees that can't happen.

Why no per-command change is needed

The MCP tools run commands via SSHManager.execCommand()client.exec(cmd, cb) with no per-channel options. I verified against the installed ssh2 1.17.0 (lib/client.js:1221) that the agent-forward channel request fires when this.config.allowAgentFwd === true OR the per-exec opts.agentForward === true. The connection-level option (agentForward: trueallowAgentFwd) therefore forwards on every exec/shell channel automatically. Setting it once at connect is sufficient.

Security

Opt-in, defaults to false. Documented in README (new SSH Agent Forwarding section) and CLAUDE.md with the standard warning: anyone with root on the remote host can use the forwarded socket to impersonate the operator for the life of the connection — mirroring ssh_config(5)'s ForwardAgent caution. No new files, no prompts, no behaviour change for existing configs.

Tests / verification

  • tests/test-agent-forward.js (new, npm run test:agentforward) — drives connect() with a stubbed ssh2 client and asserts the wiring across four states: forwarding on (agentForward set), explicitly off, absent (default off), and the critical forwardAgent:true but no SSH_AUTH_SOCK case where neither agent nor agentForward is set (so ssh2 never throws).
  • tests/test-config-field-names.js — extended to cover forwardAgent for both .env and TOML, the boolean coercion (incl. falsefalse), and a TOML export→reload round-trip.
  • Full suite green locally: npm test ✅ · ./scripts/validate.sh ✅ · npx knip ✅ · npx eslint src/ 0 errors.

Closes #52

Add an opt-in per-server ForwardAgent equivalent so processes on the
remote host can authenticate to other SSH hosts using the keys in the
local ssh-agent (e.g. git over SSH on a remote server with a local
GitHub key), without copying any private key to the server. Requested
in #52 by @raphaelbahat.

- config-loader: read FORWARD_AGENT (.env) / forward_agent (TOML) into a
  camelCase forwardAgent field, emit it in exportToToml. A parseBool()
  helper coerces the .env string so "false" is not treated as truthy.
- ssh-manager: set connConfig.agentForward only inside the SSH_AUTH_SOCK
  block, since ssh2 throws if agentForward is set without a valid agent.
  ssh2's connection-level allowAgentFwd forwards on every exec/shell
  channel, so no per-command change is needed.

Defaults to false — fully opt-in, no behaviour change for existing
configs. Documented with the standard ForwardAgent security warning in
README and CLAUDE.md.

Tests: test-agent-forward.js drives connect() with a stubbed ssh2 client
across the four states (on / off / absent / no-agent), and
test-config-field-names.js gains forwardAgent coverage incl. env boolean
coercion and a TOML export round-trip.

Closes #52
@bvisible
bvisible merged commit 64eae38 into main Jul 13, 2026
5 checks passed
@bvisible
bvisible deleted the feat/forward-agent branch July 13, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ForwardAgent support per server config

1 participant