termique
Blog
Deep dive9 min read

SSH agent forwarding: convenience vs the security risk

How ssh agent forwarding actually works, the real hijacking scenarios it opens up, and safer alternatives like ProxyJump and short-lived ssh certificates.

SSH agent forwarding: convenience vs the security risk

You SSH into a bastion host, then need to reach a git server or a second internal box from there, and typing your key’s passphrase again feels like friction you shouldn’t have to deal with. So you turn on agent forwarding once, forget about it, and never think about it again.

That’s exactly the setup that turns one compromised intermediate host into full access to every system your key can reach. ssh agent forwarding security risks aren’t hypothetical: the mechanism ssh uses to skip re-entering credentials on every hop is the same mechanism that lets anyone with root on a host you forwarded to make signing requests as you, for as long as your session stays open. This piece breaks down how forwarding actually works, when it’s genuinely dangerous, and what to use instead.

What are the security risks of ssh agent forwarding?

The core risk is simple to state and easy to underestimate: agent forwarding hands the remote host a way to ask your local ssh-agent to sign things on your behalf, for as long as the connection to that host stays open. Whoever controls that host controls your agent for that window. Not your private key file, they never get that, but the ability to prove they’re you to any system that trusts your key.

How agent hijacking actually works

When ForwardAgent is enabled, ssh opens a Unix domain socket on the remote host and points the SSH_AUTH_SOCK environment variable at it. That socket is tunneled back over your existing encrypted connection to your local ssh-agent. Anyone who can read or write to that socket, meaning anyone with your user’s permissions or root on that host, can send it signing requests exactly like your own ssh client would. They don’t need your passphrase or your key file. They just need the socket, and it’s sitting in /tmp for the length of your session.

That’s a meaningfully different exposure than an on-device, end-to-end encrypted credential store, where the private material never leaves a single machine in any form, forwarded or not. See end-to-end encrypted ssh credential storage: how it actually works for how that model holds up by comparison.

Why “the host I trust won’t get compromised” isn’t a real defense

The threat model for agent forwarding isn’t about the host you’re already logged into behaving badly. It’s about that host getting compromised by something else entirely: a vulnerable package, a stolen credential belonging to a different user, an over-permissioned CI job, while your session happens to be open. Forwarding doesn’t care why the host went bad. It extends trust for as long as the socket exists, and that’s a window you don’t control.

How does ssh agent forwarding actually work?

Agent forwarding is meant to solve one specific problem: reaching a second host, or a service like a private git remote, from inside a session on a first host, without copying your private key onto that first host. On paper that’s a reasonable goal, keeping the key itself off the intermediate machine. In practice it works by extending your agent’s reach rather than your key’s, and that distinction is where the risk lives.

The -A flag and what actually gets forwarded

Running ssh -A user@host, or setting ForwardAgent yes in your ssh config for that host, does not copy your private key anywhere. What it forwards is access to your agent’s signing capability. Every key currently loaded in your local ssh-agent, not just the one you used to authenticate to that host, becomes reachable from whatever runs on the remote host for the session’s duration.

Host bastion
  HostName bastion.example.com
  ForwardAgent yes

That config line is the whole trigger. If you loaded three keys into your agent this morning for three unrelated projects, all three are reachable from bastion for as long as you’re connected, whether or not the session on bastion has anything to do with the other two.

Checking whether you’re already forwarding by default

It’s worth auditing your own config before assuming forwarding is off. A wildcard host entry from an old tutorial or a copied dotfile is a common way this ends up enabled everywhere without anyone deciding that on purpose:

grep -ri forwardagent ~/.ssh/config /etc/ssh/ssh_config.d/*.conf 2>/dev/null

If that turns up a bare Host * block with ForwardAgent yes, every host you SSH into gets your agent forwarded, not just the one bastion you meant it for when you first set it up.

Real-world agent hijacking scenarios you should know about

The mechanics above sound abstract until you map them onto how bastion and shared-access setups actually get used.

A compromised jump host pivoting to production

The classic case: a small internal jump host gets root-compromised through an unrelated vulnerability, days before you even connect to it. You SSH in with agent forwarding on, meaning to reach a production database server behind it. The attacker, already sitting on the jump host, watches SSH_AUTH_SOCK appear and issues a signing request to your agent to authenticate onward to production, using your identity, without ever seeing your private key or your passphrase.

Shared multi-tenant servers where other users have root

The same mechanic applies to any shared box where you don’t fully control who has elevated access: a shared dev server, a contractor’s staging environment, a host managed by a team you don’t work with day to day. If forwarding is on and someone else on that box has root, or gets it, your forwarded agent is theirs to use for the length of your session, silently and without leaving obvious signs on your end.

A build agent that forwards further than it needed to

A less obvious version shows up in CI. A build runner configured with agent forwarding so it can clone a private repository ends up, later, also used to deploy to staging and, on a bad day, to production, because someone reused the same runner config rather than scoping a separate credential per stage. Anything that can execute on that runner, a malicious dependency in the build, a compromised plugin, inherits whatever your forwarded agent can reach, not just the git remote it was meant for.

When is ssh agent forwarding still worth the risk?

None of this means agent forwarding is never appropriate. There’s a small set of legitimate cases where forwarding to a specific, tightly scoped host for a specific task is a reasonable tradeoff, provided you understand exactly what you’re extending trust to.

  • Pulling from or pushing to a private git remote from inside a remote deploy or build host, where copying a deploy key onto that host is worse than briefly forwarding
  • Running a one-off script on a remote host that itself needs to SSH onward to a second host you control, where setting up a dedicated jump-host path isn’t worth it for a single task
  • Working on a host you personally administer end to end, where “who has root” is a known, short list, not an open question

Outside cases like these, on a bastion you share with a team, a host you don’t fully control, or anything internet-facing, the convenience isn’t worth what you’re handing over.

What’s a safer alternative to ssh agent forwarding?

In most cases the problem people reach for agent forwarding to solve, multi-hop access through a bastion, has a safer answer that doesn’t touch your agent at all.

ProxyJump and ProxyCommand: multi-hop without exposing your agent

ProxyJump solves the actual problem agent forwarding is usually reached for without ever forwarding anything to the bastion. Your local ssh client makes two connections, both originating from your machine; the bastion just relays encrypted bytes between them and never participates in authenticating you to the far side.

Host prod
  HostName 10.0.4.12
  User deploy
  ProxyJump bastion.example.com

With this config, ssh prod transparently tunnels through bastion.example.com. No ForwardAgent, no socket on the bastion, nothing for a compromised intermediate host to intercept.

Confirming every signature with ssh-add -c

If a task genuinely requires forwarding, load only the specific key it needs, into a dedicated agent, with confirmation required on every use:

ssh-add -c ~/.ssh/deploy_key

The -c flag means every signing request, including ones sent from a forwarded socket by something you didn’t initiate, pops a local confirmation prompt before it’s honored. It doesn’t stop an attacker from asking, but it stops them from getting an answer without you noticing. If you’re already juggling several keys across several machines, see how to manage ssh credentials across multiple devices for a cleaner approach than one shared agent everywhere.

Short-lived ssh certificates instead of a forwarded agent

For teams, ssh certificates issued with a short validity window sidestep the whole problem: a host only needs a certificate signed by your CA to trust a connection, valid for minutes or hours, not a standing forwarded agent that’s reachable indefinitely. See ssh certificates vs public key auth for teams for how the tradeoffs compare in practice.

How do you detect ssh agent forwarding misuse?

This is the least comfortable part of the risk: a forwarded agent session, hijacked or not, looks identical to a normal one from the perspective of the host being authenticated to. The signing request came from a socket, and the far end just sees a valid signature, no username, no source IP for the actual attacker, no distinguishing mark. Standard sshd logs on the target won’t tell you a request originated from a hijacked forward rather than your own machine.

Commands like w or last on the target host won’t distinguish a signature that arrived through a forwarded socket from one your own machine produced directly. Correlating a suspicious authentication with a specific forwarding session usually means cross-referencing timestamps across sshd logs on both the intermediate host and the target, and that’s only possible if both hosts are logging in the first place.

This is exactly the gap per-command audit logs for shared ssh access is built to close: logging what ran and when at the command level, on the host itself, independent of how the connection got authenticated, so a shared bastion doesn’t become a blind spot.

A practical checklist for securing ssh access without agent forwarding

  • Default ForwardAgent to no in your global ssh config, and only enable it per-host where it’s explicitly needed
  • Reach for ProxyJump first for any multi-hop access pattern, before reaching for forwarding
  • If forwarding is unavoidable, use a dedicated agent holding only the one key needed, added with ssh-add -c
  • Prefer short-lived ssh certificates over long-lived forwarded keys for team and bastion access
  • Keep per-command audit logging on shared or bastion hosts so a hijacked session leaves a trail
  • Treat “I trust this host” as a today-only statement: a host’s trust level can change the moment it’s compromised, without you finding out immediately

termique doesn’t forward agents between hosts at all. Its SSH key vault keeps private keys in the OS keychain on your device, generated and managed locally, and when you share a host with a teammate it shares access through an encrypted invite scoped to that one host, not a live signing socket reachable from wherever the session happens to be open. Combined with termique’s per-command audit log, that’s bastion-style shared access without the blind spot agent forwarding creates. See the complete guide to ssh security for the rest of the model termique is built around.

Try termique free.

SSH manager with end-to-end encrypted credentials, AI assistant, and cross-device sync.

Download free

Keep reading

All articles ⟶