7 min read

SSH agent forwarding risks in AI-assisted development

SSH agent forwarding risks grow in AI-assisted workflows. Learn how remote hosts reuse signing authority, how to disable it, and safer SSH patterns.

SSH agent forwarding risks in AI-assisted development

SSH agent forwarding risks are easy to underestimate because the private key stays on the developer's machine. That fact is true, and it is not enough. A remote host that receives a forwarded agent socket can ask the local agent to sign SSH authentication challenges. For every system that accepts that identity, the remote host may be able to act as you for as long as that access lasts.

AI-assisted development makes this mistake easier to create and harder to notice. A coding agent can open remote shells, run deployment commands, inspect repositories, and follow an SSH config you wrote years ago for an interactive session. If the agent reaches a machine that has your forwarded socket, the boundary has already moved. Disable forwarding by default, and give remote jobs their own narrow identities when they genuinely need another hop.

A forwarded agent can authenticate beyond the first host

SSH agent forwarding exposes a signing service, not a copy of a private key. Your local ssh-agent holds one or more private identities. When you connect with forwarding enabled, SSH creates a socket on the remote machine and relays signing requests over the encrypted connection to that local agent.

That distinction often produces bad risk decisions. People hear "the key never leaves my laptop" and conclude that the remote machine has nothing worth attacking. The private bytes may remain local, but SSH authentication does not require the remote machine to possess those bytes. It needs a valid signature over an authentication request. The forwarded socket gives it a way to obtain one.

The OpenSSH ssh_config(5) manual describes ForwardAgent plainly and warns that users who can bypass file permissions on the remote host can access the forwarded agent. In practice, that means the remote account itself, a process running under that account, an administrator, or an attacker who gains sufficient control can use the socket. Root access on the remote host ends the discussion about socket permissions.

RFC 4252 describes public-key SSH authentication as a signature over connection-specific data. The server validates that signature against an authorized public key. A compromised remote host cannot turn the agent into a generic signing oracle for arbitrary documents, but it can request the SSH signatures it needs to attempt SSH logins elsewhere. That is exactly the capability an attacker wants if your identity works on source control, production hosts, or internal administration systems.

The common failure looks like this:

  1. A developer connects to build.example.net with ssh -A because that machine must reach a private repository.
  2. The developer's local agent appears on the build host through SSH_AUTH_SOCK.
  3. A build script, a compromised dependency, or another user with sufficient access asks that socket to sign for git.example.net or an internal host.
  4. The destination accepts the developer's public key and logs the new connection as the developer.

The SSH server on the destination sees a legitimate cryptographic signature. It cannot tell whether a person initiated the connection on their laptop or a process on the first remote machine requested it through forwarding. Server logs will identify the accepted key and source address, but they will not repair that lost distinction.

Configuration inheritance creates accidental exposure

Most unsafe forwarding begins in SSH configuration, not in a conscious decision made during a risky session. A single old stanza such as Host * with ForwardAgent yes can apply to every host a developer, terminal tool, or coding agent contacts. It also applies when the tool calls ssh from a script, where nobody sees a warning.

OpenSSH configuration has another sharp edge: for each parameter, the client generally uses the first value it obtains. A broad rule placed before a narrower rule can defeat the exception you thought you wrote. Do not trust a quick visual scan of ~/.ssh/config; ask SSH which configuration it will use.

Run this on the local machine before connecting:

ssh -G deploy.internal.example | grep '^forwardagent '

A safe result is:

forwardagent no

ssh -G prints the effective client configuration after it processes its files and command-line options. It does not open a connection. That makes it useful in setup checks and in a test script that reads a list of sensitive hosts.

Set an explicit deny-by-default rule near the end of the relevant config file. Put narrow exceptions before it, because OpenSSH takes the first matching value:

Host docs-bastion.example
    ForwardAgent yes

Host *
    ForwardAgent no

This example still grants a dangerous exception, so it needs a reason and an owner. The point is to make that exception visible and confined to one host rather than silently applying it to every new environment.

For a one-off connection, force the safe setting even if a config file says otherwise:

ssh -o ForwardAgent=no [email protected]

The shorter ssh -a [email protected] does the same job. Use either when connecting to a machine you have not reviewed, a temporary support host, a training environment, or a vendor-managed system.

Do not mistake IdentitiesOnly yes for a forwarding control. It affects which identities the SSH client offers when it authenticates to a server. It does not prevent SSH from creating an agent socket on the remote side. Likewise, removing SSH_AUTH_SOCK from one shell is not a complete policy. A child process may inherit another environment, and an SSH config can re-enable forwarding on the next connection.

AI workflows multiply the paths that need review

An AI coding agent does not need malicious intent to make forwarding dangerous. It only needs permission to execute a command that encounters your existing SSH habits. Agents follow repository instructions, invoke build scripts, use remote development hosts, and retry commands with small variations. Those are normal behaviors. They become risky when the environment gives them a reusable developer identity.

The concerning path is often indirect. Your local agent starts an SSH session to a development box. That session forwards your agent due to a global config rule. The coding agent runs a repository script on the development box. The script fetches a dependency or opens a second SSH connection. The second connection can use the socket that your first session placed there.

This is worse than a human typing a single git fetch because an agent can make many tool calls without stopping to question why a remote shell needs access to an unrelated destination. The agent may also encounter instructions in a repository that tell it to use a particular host alias. That alias can hide a ProxyCommand, a Match rule, or an inherited forwarding rule from the operator's view.

Treat these as separate permissions:

  • Permission for an agent to open a remote shell.
  • Permission for that remote shell to reach another SSH destination.
  • Permission for the second destination to accept the developer's identity.
  • Permission for the agent to cause the second connection.

Teams regularly collapse all four into "the agent needs SSH." That sentence does not describe the boundary. A remote shell and a reusable signing capability have different consequences and need separate decisions.

Check how the agent process starts. If it inherits SSH_AUTH_SOCK from an interactive terminal, it may already have access to local agent identities for direct SSH authentication. If it then connects to a host with forwarding enabled, that host gets a second route to those identities. A wrapper that clears the environment variable can reduce accidental local use, but it does not replace a safe SSH config or a separate identity for automation.

Also inspect agent instructions and automation wrappers for ssh -A, scp -A, or an alias that expands to them. scp and sftp rely on SSH transport settings, so a forwarding habit can spread farther than the command that first created it. Put the forwarding decision where it belongs: in the connection definition, with an explicit default of no.

A jump host does not need your agent

Many developers enable forwarding because they must cross a bastion before reaching an internal system. That was a familiar workaround when the only convenient model was "log in to the bastion, then SSH again." It remains popular because it works quickly during setup. It also turns the bastion into a machine that can reuse your identity.

Use ProxyJump when the first host only needs to carry traffic. The local client can authenticate to the final host while it tunnels through the jump host, without forwarding an agent socket to that jump host.

Host engineering-bastion
    HostName bastion.example
    User developer
    ForwardAgent no

Host release-host
    HostName release.internal.example
    User deploy
    ProxyJump engineering-bastion
    ForwardAgent no

With this arrangement, the local SSH client establishes the final SSH connection through the bastion. The bastion moves encrypted transport traffic. It does not receive a remote SSH_AUTH_SOCK that it can use to request signatures from your agent.

Test the route rather than assuming the config means what it says:

ssh -vvv release-host

In the debug output, look for the proxy jump connection and verify that it does not report agent forwarding. Then, after logging in to the final host, inspect the remote environment:

printf 'SSH_AUTH_SOCK=%s\n' "${SSH_AUTH_SOCK:-}"
ssh-add -l

An empty socket variable is the expected result when you intentionally disabled forwarding. If ssh-add -l reports no connection to an authentication agent, that is also consistent with no forwarded agent. Do not run these checks only on the final host when troubleshooting. Run them on every interactive hop where someone might have enabled forwarding.

There are legitimate cases where the jump host must itself authenticate to another host, such as a controlled release operation. That requirement means the jump host needs its own deployment identity or a short-lived workload identity. It does not mean it should borrow every identity currently loaded in a developer's laptop agent.

Remote automation needs an identity of its own

Stop borrowed developer identities
Route agent SSH through Sallyport so developer credentials remain in its vault, not the agent.

A remote build box, deployment host, or AI tool should authenticate as its assigned workload, not as the developer who happened to start the session. This is the design change that removes the need for forwarding instead of merely making it less convenient.

A workload identity should grant access only to the services that workload must use. For a source repository, use a repository-scoped deploy identity where the service supports it. For an SSH destination, authorize a dedicated public key for a dedicated account and restrict that account's commands or permissions where the server supports such limits. For a certificate-based SSH setup, issue short-lived certificates with principals that match the workload role.

SSH certificates can narrow access, but do not turn them into magic. A certificate's principal controls which accounts accept it only if the server checks principals correctly. A short validity period limits how long a signature can authenticate, but a compromised process can still use it during that window. Review the server configuration that trusts the certificate authority and the account rules that consume those principals.

Do not reuse a developer's personal public key as the "temporary" identity for a CI worker or remote agent. That keeps the audit trail ambiguous. When an authentication record says the personal key logged in, you cannot easily determine whether the developer acted, a build job acted, or a compromised remote shell acted through forwarding.

For agent-controlled HTTP and SSH actions, another pattern is to keep credentials in a local action gateway and return only the command or API result to the agent. Sallyport uses this model on macOS: its vault holds the SSH key and its sp-ssh helper performs the SSH action without giving the agent the credential itself.

The useful boundary is operational rather than rhetorical. The agent requests a named action, the gateway applies the credential, and the agent receives output. The agent does not receive an agent socket that it can pass into an unrelated remote process. That makes approvals and records meaningful because they correspond to an action, not an open-ended ability to ask for future signatures.

Confirmation prompts reduce exposure but do not fix trust

Sometimes a short maintenance task genuinely requires forwarding and no dedicated identity is ready. In that case, forward as little signing authority as possible and make every remaining use visible. This is a temporary control, not a permanent architecture.

Start a separate agent socket rather than forwarding the agent that holds your daily collection of identities. Add only the identity needed for the maintenance task, with a short lifetime and confirmation requirement:

ssh-agent -a "$HOME/.ssh/maintenance-agent.sock" > "$HOME/.ssh/maintenance-agent.env"
. "$HOME/.ssh/maintenance-agent.env"
ssh-add -c -t 900 ~/.ssh/maintenance_ed25519
ssh -o ForwardAgent=yes [email protected]

ssh-add -c asks for confirmation before the agent signs. -t 900 removes the identity after 15 minutes. Check the ssh-add(1) manual on your installed OpenSSH version, because confirmation behavior depends on the local agent and its user interface.

Use a separate shell for this task. When the work ends, remove the identity and terminate that agent:

ssh-add -D
ssh-agent -k

This sequence prevents later requests from that temporary socket. It does not revoke signatures already issued, open connections already authenticated, or copies of data a remote process already obtained. Close the remote session and check the destinations that identity could access.

OpenSSH also supports destination constraints through ssh-add -h in versions that include the feature. These constraints can restrict which host paths an agent will sign for, based on host keys in known_hosts. They are worth evaluating for a controlled environment, but they add configuration dependencies that teams often fail to maintain. A stale or incomplete known_hosts file turns a safety measure into an outage at the worst possible time. Test it with the exact jump path and host aliases your automation uses.

Do not rely on prompt fatigue as a security boundary. A compromised host can ask for signatures repeatedly and name destinations that resemble normal infrastructure. If operators approve prompts quickly to finish an incident, confirmation protects less than they think. A narrow identity and a short lifetime give the prompt a smaller blast radius.

Logs must distinguish sessions from SSH actions

Gate MCP-triggered SSH actions
Agents connect through sp mcp, while Sallyport performs SSH through the bundled sp-ssh helper.

SSH authentication logs tell you that a key authenticated to a server. They rarely tell you why the signature was requested or whether agent forwarding enabled the path. If you allow automated remote actions, capture enough information to reconstruct who launched the agent, which process received approval, which destination it contacted, and what action it requested.

Keep session-level records separate from action-level records. A session record answers which agent process received access and when that access ended. An action record answers which SSH command or API call ran under that access. Blending both into one generic event log makes investigations slow because an operator must infer a chain of cause and effect from fragments.

On an SSH server, review its normal authentication records after any suspicious forwarding event. The exact source depends on the operating system and service configuration, but common places include system journal entries and the SSH daemon's authentication log. Look for the accepted public-key fingerprint, account name, source address, and time. Compare those with the first remote host's session history.

Do not claim certainty from an IP address. A forwarded connection may originate from a build host, a bastion, a network address translation gateway, or a private overlay. The server can establish where the TCP connection came from, not who initiated the signature request on the developer's machine.

Tamper-evident records help only if the system writes them outside the agent's control. A process that can edit its own action history can erase the suspicious second hop before anyone looks. Sallyport records agent sessions and individual calls from one encrypted, hash-chained audit log, and sp audit verify can check that chain offline without a vault key.

Whatever tooling you choose, test its record against a real failed authorization and a real successful SSH command. Confirm that it names the agent run, destination, credential reference or fingerprint, result, and time. Logs that merely say "tool completed" cannot answer an incident question about identity reuse.

Treat an exposure as authorization abuse, not automatic key theft

Identify approved agent runs
The Sessions journal records agent runs and lets you revoke a run instantly.

If you discover that you forwarded an agent to a host you do not trust, respond as though that host may have used your identity while the session existed. Do not wait for proof that it extracted a private key. The meaningful risk is unauthorized authentication, and the private key may never have left your machine.

First stop future use. Close all SSH sessions to that host, remove the exposed identity from the agent, and disable the host-specific forwarding rule. If the identity was loaded into a shared agent, do not reflexively run ssh-add -D on a workday and declare victory. That may interrupt legitimate sessions while leaving the affected public key authorized across servers.

Then remove or revoke authorization at the destinations the identity can reach. For ordinary authorized-key setups, remove the public key from accounts where it should no longer work and replace it with a new one where needed. For SSH certificates, revoke the certificate according to your CA and server process, or let a short-lived certificate expire if you can verify that the exposure window is acceptable. For repository access, revoke or replace the corresponding deploy or user credential through that service's normal controls.

Investigate a bounded time window: from when forwarding became available until the remote session ended or the local agent stopped accepting requests. Review destination authentication records, remote shell history where it is trustworthy, job records, and action logs. Preserve logs before cleaning up the remote host if you suspect compromise.

Finally, fix the path that permitted it. If a global ForwardAgent yes caused the event, changing only the compromised host entry leaves the next unknown host exposed. If an AI workflow inherited a personal socket, give that workflow an explicit connection setup and a dedicated workload identity. The repair should make the unsafe route impossible by default, not merely remind people to remember a flag.

The safe default should survive hurried work

Forwarding persists because it removes friction in the moment. A developer needs one more hop, a build needs a private fetch, or an agent must complete a task before a deadline. Those are real needs. They do not justify placing a broadly trusted developer identity on every machine that happens to be in the path.

Set ForwardAgent no globally. Use ProxyJump for transport-only bastions. Assign remote automation an identity that says what it is allowed to do. When a temporary exception cannot be avoided, isolate one identity, require confirmation, set a short expiry, and remove it when the task ends.

Run ssh -G against the host aliases your team uses this week. That small check catches the quiet configuration mistake before a remote process gets a signing service it was never meant to have.

FAQ

Can a remote server steal my private key through SSH agent forwarding?

Usually, no. The remote machine cannot read the private key from a normal forwarded agent socket, but it can ask your local agent to sign authentication challenges while the connection remains available. That signing ability is enough to log in to systems that accept the identity.

Does ssh -A enable agent forwarding?

No. ssh -A explicitly enables forwarding, while ssh -a disables it for that connection. A config file can still surprise you, so inspect the effective value with ssh -G host | grep '^forwardagent '.

Do I need agent forwarding when I use ProxyJump?

ProxyJump creates a transport path through an intermediary; it does not require your agent to be available to the intermediary. Configure the final destination's authentication locally and leave ForwardAgent no in place. Treat a jump host as a route, not as a workstation you must equip with your identity.

Does IdentitiesOnly prevent SSH agent forwarding?

IdentitiesOnly yes controls which identities the SSH client offers during authentication. It does not stop the client from forwarding an agent socket after login. Set ForwardAgent no separately.

Is ssh-add -c enough protection for forwarded agents?

A confirmation-constrained identity reduces silent reuse because your local agent asks before each signature. It does not make an untrusted remote host safe: the host can create repeated prompts, and a rushed approval still authorizes a real login attempt. Use it as a temporary brake, not as your normal design.

How can I forward an SSH agent more safely for a temporary task?

Use a separate agent with only a narrowly scoped identity, add a short lifetime, require confirmation, and forward it only to a named host for a short task. Remove the identity when the task ends. A broad personal agent with several long-lived keys is the wrong thing to forward.

Can an AI coding agent misuse my forwarded SSH identity?

Your exposure depends on what the agent inherited and what the remote account can execute. An agent that can run shell commands may invoke ssh, inherit SSH_AUTH_SOCK, or open a remote session that receives a forwarded socket. Give the agent a purpose-built remote access path instead of your interactive developer identity.

What should I do if I forwarded my SSH agent to an untrusted host?

First remove the exposed public key from systems where it grants access, or revoke the certificate if you use SSH certificates. Then inspect authentication records on those systems and rotate the identity if you cannot bound its use. Killing the local agent stops future requests but cannot undo signatures already made.

Is SSH agent forwarding safe in CI?

Forwarding can work on a CI runner, but it gives the runner's workload a route to request signatures from the agent. Prefer a deploy credential owned by the runner, a short-lived certificate, or an action gateway that performs the SSH operation itself. The runner should never borrow a developer's daily identity.

How do I check whether SSH agent forwarding is active?

Run ssh -G target | grep '^forwardagent ' on the client to see the effective setting before connecting. On the remote host, a nonempty SSH_AUTH_SOCK and usable output from ssh-add -l indicate that an agent is available to that session. Check both sides when investigating a suspicious path.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov