SSH Agent Forwarding Hijacking on Debian 13

updated 10 min read

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:

  1. List /tmp/ssh-*/agent.* to find active forwarded sockets.
  2. Point an SSH client at one of them.
  3. 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 setupWhat root on the jump host can doExposure
Plain key in agent, ForwardAgent yesSign silently for the whole session, to any host the key opensFull
Key added with ssh-add -t 1hSame, but only until the lifetime expiresFull, shorter window
Key added with ssh-add -cTrigger a confirmation dialog on your desktop; succeeds only if you click OKLow, if you read the prompt
Key added with ssh-add -h destination constraintsUse the key only for the hops you listedLimited to the listed destinations
ed25519-sk / ecdsa-sk FIDO key (user presence required)Each signature needs a physical touch on your tokenLow
FIDO key with no-touch-requiredSign silentlyFull
ProxyJump, no forwardingOnly sees an encrypted TCP stream. No socket existsNone via the agent
Private key file copied to the jump hostRead the key and keep itWorse 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 ProxyJump for bastions and set Host * ForwardAgent no. Check with ssh -G.
  • When you must forward, use ssh-add -c -t and destination constraints (-h).
  • Prefer ed25519-sk keys with touch, and use verify-required where the token supports it.
  • On servers, set AllowAgentForwarding no and AllowStreamLocalForwarding no by default, with Match Group exceptions. Check with sshd -T -C.
  • Hunt with find /tmp/ssh-*, ss -xp, lsof -U, and Accepted publickey lines 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

Comments