The setup is familiar to anyone who administers more than one machine. You keep your key on your laptop, unlocked in ssh-agent. You connect to a jump box. From there you need to reach a second machine, or pull from a private git repository, and you would rather not copy your key onto the jump box. So you pass -A, or set ForwardAgent yes, and authentication from the jump box simply works.
What happens underneath: the SSH client on the far end creates a Unix domain socket, sets SSH_AUTH_SOCK in your shell environment to point at it, and tunnels everything written to that socket back down the connection to the agent on your laptop. When something on the remote host needs a signature, it writes a challenge into the socket, your laptop's agent signs it, and the signature comes back.
The key material genuinely stays put. The capability does not.
Who can use the socket
The socket lives in a directory owned by you with restrictive permissions, which stops other unprivileged users on the box. It does not stop root. Anyone with root on that machine, or anyone who has compromised a process running as your user, can set SSH_AUTH_SOCK to your socket path and start authenticating as you to anything your key opens.
They cannot extract the key, so the window closes when you disconnect. Inside that window the difference is academic. Your agent will happily sign a challenge from your production database host while you are reading logs on a staging box, and the default configuration gives you no prompt, no log line, and no indication afterwards that it happened.
Agent forwarding is not "my key stays safe." It is "for the next twenty minutes, this machine can authenticate as me anywhere my key is authorised, and I will not be told when it does." Whether that trade is acceptable depends entirely on who else has access to the machine.
The Matrix.org breach is the worked example
In April 2019 the Matrix.org homeserver infrastructure was compromised. The published post-mortem describes an entry point through a vulnerable Jenkins instance, and then a lateral move that used a forwarded SSH agent from an administrator session on that host to reach production systems. The attacker did not need to crack a key. Somebody had connected to the CI box with forwarding on, and the credential was there for the taking.
This is the usual sequence. The forwarded agent is rarely the initial compromise. It is the thing that turns a foothold on a low-value machine into access on a high-value one, which is exactly the boundary a jump host is supposed to enforce.
CI runners are the most common offender: shared, frequently patched late, running third-party build steps, and administered by people who forward agents out of habit. The same reasoning applies to a shared bastion, a colleague's workstation, and any host whose administrators you have not personally vetted. The pattern belongs to the same family as supply chain compromise: the trust you extend to a system quietly becomes trust extended to everyone who has ever had a shell on it.
ProxyJump does the thing you actually wanted
Most agent forwarding exists to solve a routing problem: the target is only reachable through an intermediate host. That has had a proper solution since OpenSSH 7.3 in 2016, and it does not involve exposing your agent at all.
ssh -J bastion.example.com target.internal, or a ProxyJump line in your config, tells your local client to open a connection to the bastion and then tunnel a second, independent SSH session through it to the target. Both authentication handshakes happen on your laptop. The bastion forwards encrypted bytes and never sees a signing request, because there is nothing on the bastion to send one.
| Agent forwarding | ProxyJump | |
|---|---|---|
| Private key location | Local only | Local only |
| Intermediate host can authenticate as you | Yes, for the session | No |
| Host key of the target verified by you | No, by the intermediate | Yes, locally |
| Works for git operations run on the remote host | Yes | No, different problem |
That last row is the real limitation. If you need to run git pull on the remote machine against a private repository, ProxyJump does not help, because the process needing credentials is genuinely over there. That case deserves its own credential: a deploy key scoped to one repository, or a short-lived certificate, rather than the key that opens your entire estate.
When you do forward, narrow it
- Never set
ForwardAgent yesunderHost *. This is the single most common misconfiguration, and it means every host you touch, including one you SSH into once to check something, gets the capability. Set it per host, deliberately. - Add keys with confirmation.
ssh-add -cmakes the agent prompt you locally for every signing request. Unexpected prompts become visible, and a silent lateral move becomes a dialog box you did not expect. - Add keys with a lifetime.
ssh-add -t 4hexpires the key from the agent, which bounds the damage from a forgotten unlocked session. - Use destination-constrained keys. OpenSSH gained this in 2022:
ssh-add -hbinds a key so the agent will only sign for a specified onward destination, and refuses if the request comes from a host outside the declared path. This is the feature that makes forwarding defensible rather than merely convenient. - Prefer hardware-backed keys. A key on a security token still needs a touch for each signature, which turns an invisible use into a physical one. The same reasoning that favours hardware keys over authenticator apps applies here.
- Separate keys by blast radius. One key for build infrastructure, another for production, another for personal repositories. Forwarding a scoped key into a shared box is a much smaller decision than forwarding the key that opens everything. Certificate-based SSH makes this easier to run at scale, since certificates expire on their own.
The general principle
Delegation and disclosure are different risks, and this feature confuses them. Copying a key to a server is disclosure and everybody recognises it as bad. Forwarding an agent is delegation, it feels safe because nothing is copied, and it hands out a live capability instead of a stored secret. The capability is time-bounded, and within its window it is the same power.
The useful question before typing -A is not "do I trust this machine with my key," because you are not giving it your key. It is: do I trust everyone who currently has root on this machine to authenticate as me for the next hour? On your own laptop the answer is yes. On a shared CI runner the answer is almost certainly no, and that is the machine where people forward most often.