Many admins type ssh -A jump once and forget about it. This post explains SSH agent forwarding hijacking: anyone with root on the jump host, or anyone running as your user there, can use your forwarded agent to log in to other servers as you. They never copy your private key, and nothing unusual shows up on your laptop. Debian 13 ships OpenSSH 10.0 (at the time of writing, packages.debian.org lists openssh-server 1:10.0p1-7+deb13u4). That version still puts forwarded agent sockets in /tmp, so the mechanism and detection below match what you have on trixie today.
This is not a CVE. It is documented behaviour, and the fixes are configuration changes. General sshd hardening is covered in Hardening SSH on Debian 13 with OpenSSH 10, so this post only deals with the agent.
How SSH agent forwarding hijacking works
Your keys live in ssh-agent on your workstation. When you connect with ForwardAgent yes or -A, the client asks the server to open an agent channel. On the server, sshd (in auth_input_request_forwarding() in session.c) creates a directory with mkdtemp() from the template /tmp/ssh-XXXXXXXXXX. Inside it, sshd creates a listening UNIX socket named agent.<pid>, owned by your user, and exports its path as SSH_AUTH_SOCK in your session.
Any program on the jump host that connects to that socket is talking to the agent on your laptop. Requests go back through the encrypted SSH connection, the agent signs the authentication challenge, and the signature comes back. The private key never leaves your machine. The ssh_config(5) man page states the consequence directly: an attacker who can bypass file permissions on the remote socket cannot extract key material, but can use the loaded identities to authenticate.
There are only two protections: the directory permissions and the socket owner. Neither stops root. The ssh-agent(1) man page also says the socket can be abused by root or by another instance of the same user. In practice an attacker who has root on the jump host only needs to:
- List
/tmp/ssh-*/agent.*to find active forwarded sockets. - Point an SSH client at one of them.
- Ask the agent which keys it holds, then log in to whatever those keys open.
This works for as long as your session on the jump host stays open. Without extra constraints, every signature happens silently. Because the request comes from your own agent, the logins on the target hosts look exactly like your normal logins.
OpenSSH 10.1 (released 2025-10-06) moved both ssh-agent and forwarded sshd sockets from /tmp to ~/.ssh/agent. That helps against sandboxed processes that cannot reach your home directory. It does nothing against root. If you run 10.1 or later from another source, look there instead of /tmp.
Which key setups are exposed
| Client setup | What root on the jump host can do | Exposure |
|---|---|---|
Plain key in agent, ForwardAgent yes | Sign silently for the whole session, to any host the key opens | Full |
Key added with ssh-add -t 1h | Same, but only until the lifetime expires | Full, shorter window |
Key added with ssh-add -c | Trigger a confirmation dialog on your desktop; succeeds only if you click OK | Low, if you read the prompt |
Key added with ssh-add -h destination constraints | Use the key only for the hops you listed | Limited to the listed destinations |
ed25519-sk / ecdsa-sk FIDO key (user presence required) | Each signature needs a physical touch on your token | Low |
FIDO key with no-touch-required | Sign silently | Full |
ProxyJump, no forwarding | Only sees an encrypted TCP stream. No socket exists | None via the agent |
| Private key file copied to the jump host | Read the key and keep it | Worse than hijacking |
How to detect agent socket abuse
Run these commands as root on the jump host. Assume a careful attacker will hide these traces, so treat every result as a lead to investigate, not as proof either way.
List forwarded agent sockets
find /tmp -maxdepth 2 -type s -path '/tmp/ssh-*/agent.*' -printf '%u %TY-%Tm-%Td %TH:%TM %p\n'
Each line is a user with an open, forwarded agent. Compare the list with who or loginctl list-sessions. Every socket should belong to a live session of that user. Since sockets are named after the sshd process ID, a socket whose session has gone away deserves a closer look.
Find who is connected to the socket
sshd holds the listening end. Any extra connection on the same path means a client is using the agent at that moment:
ss -xp | grep '/tmp/ssh-'
lsof -U +c 0 2>/dev/null | grep '/tmp/ssh-'
lsof tells you which process owns the named socket, which should be sshd. A client connecting to a UNIX socket gets an unnamed socket, so the client usually doesn't appear under the path. To find the client, take the inode from the Peer Address:Port column of the ss line and search for it:
ss -xp | grep -w 123456
Replace 123456 with the peer inode. The process shown is the one talking to the agent. An ssh or ssh-add process run by root or by another user, connected to your socket, is the signature of a hijack.
Look for borrowed SSH_AUTH_SOCK values
Low-effort attackers set SSH_AUTH_SOCK in their environment. This loop reports processes whose agent socket belongs to a different user:
for f in /proc/[0-9]*/environ; do
sock=$(tr '\0' '\n' < "$f" 2>/dev/null | sed -n 's/^SSH_AUTH_SOCK=//p')
[ -n "$sock" ] || continue
pid=${f#/proc/}
pid=${pid%/environ}
puser=$(stat -c %U "/proc/$pid" 2>/dev/null)
sowner=$(stat -c %U "$sock" 2>/dev/null)
if [ -n "$sowner" ] && [ "$puser" != "$sowner" ]; then
echo "PID $pid ($puser) uses $sock owned by $sowner"
fi
done
Read the sshd journal on the targets
On Debian the unit is ssh.service. sshd logs every successful public key login along with the key type and fingerprint. A hijacked login shows your key fingerprint, coming from the jump host IP, at a time you weren't connecting:
journalctl -u ssh --since '2026-10-01' -g 'Accepted publickey'
journalctl -u ssh -g 'Accepted publickey for .* from 192\.0\.2\.10 '
Accepted publickey for <user> from <jump-host-ip> port <port> ssh2: ED25519 SHA256:<fingerprint>
Match those lines against your session windows on the jump host (journalctl -u ssh -g 'session opened|session closed' there). Avoid raising sshd to LogLevel DEBUG just to see agent channel requests. sshd_config(5) says debug logging violates users' privacy and is not recommended.
How to stop agent forwarding hijacking
1. Use ProxyJump instead of ForwardAgent
If you only forward the agent to hop through a bastion, you don't need forwarding. ProxyJump (-J) opens a TCP tunnel through the jump host and runs authentication end to end from your laptop. The jump host never gets an agent socket.
Host jump
HostName jump.example.org
ForwardAgent no
Host *.internal
ProxyJump jump
ForwardAgent no
Host *
ForwardAgent no
Verify the effective client config, then confirm the jump host has no socket for you:
ssh -G app01.internal | grep -E '^(forwardagent|proxyjump) '
ssh jump 'echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK"'
The first command should print forwardagent no and proxyjump jump. The second should print an empty value. If key authentication fails after the switch, see SSH Permission denied (publickey) on Debian 13.
2. Require confirmation with ssh-add -c
For hosts where you really need a forwarded agent, for example to run git pull on a server, add the key with confirmation and a lifetime. The agent then runs an askpass program for every use, and only an exit status of zero counts as approval. Install one on the machine that runs the agent:
sudo apt install ssh-askpass
ssh-add -c -t 1h ~/.ssh/id_ed25519
Verify: ssh -A deploy.example.org true must show a dialog on your desktop before the login completes. If a dialog appears when you didn't start anything, click no, because someone is using your socket. This needs a graphical session on the client. Use one of the desktop variants (ssh-askpass-gnome, ksshaskpass, lxqt-openssh-askpass) if it fits your desktop better.
3. Constrain where the key may be used
Destination constraints (ssh-add -h, OpenSSH 8.9 or later at both ends) bind a key to specific hops. Keep the forwarded key away from everything else:
ssh-add -c -h 'jump.example.org' -h 'jump.example.org>git@git.example.org' ~/.ssh/id_ed25519_deploy
The client must already have the host keys of all listed hosts in known_hosts. Verify from the jump host: ssh git@git.example.org works, and ssh other.internal is refused.
4. Use FIDO2 sk keys that need a touch
An ed25519-sk key requires user presence on the token for every signature by default. A hijacker's request just waits for a touch that never comes, or makes the token blink when you didn't expect it to. verify-required also asks for a PIN, if your authenticator supports it:
ssh-keygen -t ed25519-sk -O verify-required -f ~/.ssh/id_ed25519_sk
ssh-keygen -lf ~/.ssh/id_ed25519_sk.pub
The fingerprint line should end with (ED25519-SK). Never create these keys with no-touch-required. On servers, you can add verify-required as an option in front of the key in authorized_keys.
5. Restrict forwarding on the server
On jump hosts and shared boxes, refuse agent forwarding by default and allow it per group. Debian's sshd_config includes /etc/ssh/sshd_config.d/*.conf at the top, and sshd uses the first value it reads for each keyword. That means a drop-in overrides the main file. End the drop-in with Match all so the last block doesn't swallow the directives that follow it.
# No forwarded agents and no forwarded UNIX sockets by default
AllowAgentForwarding no
AllowStreamLocalForwarding no
# Bastion users: local TCP forwarding to port 22 only, enough for ProxyJump
Match Group jumpusers
AllowTcpForwarding local
PermitOpen *:22
# The few accounts that genuinely need a forwarded agent
Match Group agent-forward
AllowAgentForwarding yes
Match all
The man page warns that disabling agent forwarding does not improve security unless users are also denied shell access, because they can always install their own forwarders. AllowStreamLocalForwarding no above closes the obvious alternative of forwarding the agent socket as a UNIX-socket forward. However, sshd_config(5) gives the same warning for AllowStreamLocalForwarding and for AllowTcpForwarding, so a user with a shell can get around all of these settings. They only really help for accounts without shell access, for example accounts limited by ForceCommand or a restricted shell. For other accounts, use DisableForwarding yes or take away shell access. OpenSSH 10.0 fixed a bug where DisableForwarding failed to disable agent and X11 forwarding, so it works as documented on Debian 13. To block forwarding for a single key, put restrict or no-agent-forwarding in front of it in authorized_keys.
Test the config and check the effective values for a sample user before reloading:
sudo sshd -t
sudo sshd -T -C user=alice,host=laptop.example.org,addr=192.0.2.50 | grep -E '^(allowagentforwarding|allowstreamlocalforwarding|allowtcpforwarding|permitopen) '
Warning: a wrong sshd config can lock you out. Keep an existing root session open, reload rather than restart, and test a new login before you close anything:
sudo systemctl reload ssh
Verify from a client: ssh -A jump 'echo "$SSH_AUTH_SOCK"' prints an empty line for normal users, and find /tmp -maxdepth 2 -type s -path '/tmp/ssh-*/agent.*' on the jump host finds nothing for them.
Takeaways
- A forwarded agent gives root on the remote host your login rights for as long as the session lasts. Your key isn't copied, but that doesn't make this safe.
- Use
ProxyJumpfor bastions and setHost *ForwardAgent no. Check withssh -G. - When you must forward, use
ssh-add -c -tand destination constraints (-h). - Prefer
ed25519-skkeys with touch, and useverify-requiredwhere the token supports it. - On servers, set
AllowAgentForwarding noandAllowStreamLocalForwarding noby default, withMatch Groupexceptions. Check withsshd -T -C. - Hunt with
find /tmp/ssh-*,ss -xp,lsof -U, andAccepted publickeylines from your jump host IPs. - On OpenSSH 10.1 and later, the sockets move to
~/.ssh/agent. Update your detection paths when you upgrade.
Sources
- ssh_config(5) – Debian trixie manpages
- sshd_config(5) – Debian trixie manpages
- sshd(8) – Debian trixie manpages
- ssh(1) – Debian trixie manpages
- ssh-add(1) – Debian trixie manpages
- ssh-agent(1) – Debian trixie manpages
- ssh-keygen(1) – Debian trixie manpages
- openssh-server package in Debian trixie
- ssh-askpass package in Debian trixie
- OpenSSH 10.1 release notes
- OpenSSH 10.0 release notes
- OpenSSH: SSH agent restriction
- openssh-portable session.c (V_10_0_P1)
- openssh-portable auth.c (V_10_0_P1)
Comments