DEV Community

Cover image for SSH Agent Forwarding vs ProxyJump: Why Agent Forwarding Is Dangerous and What to Use Instead
Mahafuzur Rahaman
Mahafuzur Rahaman

Posted on

SSH Agent Forwarding vs ProxyJump: Why Agent Forwarding Is Dangerous and What to Use Instead

Thousands of tutorials recommend ForwardAgent yes. Most of them don't tell you what it actually does to your security posture. Here's the full picture.


You need to SSH from your laptop to a bastion, then from the bastion to an internal server. You've seen the solution in a dozen tutorials:

Host bastion
    ForwardAgent yes
Enter fullscreen mode Exit fullscreen mode

It works. It's convenient. And it creates a security hole that could let anyone with root on the bastion impersonate you to every server your key unlocks — for as long as your session is open.

This isn't a theoretical risk. It's a well-documented attack vector with a name: SSH agent hijacking. And the fix — ProxyJump — has been available since 2017 and solves the same problem without the exposure.

This article explains exactly what agent forwarding does under the hood, why it's dangerous, when (if ever) it's acceptable, and how ProxyJump eliminates the need for it in the most common use case.


What SSH Agent Forwarding Actually Does

To understand the risk, you need to understand the mechanism.

The SSH Agent

ssh-agent is a background process that holds your decrypted private keys in memory. When you run ssh host, the SSH client asks the agent to perform cryptographic operations (signing challenges) on its behalf. Your private key never leaves the agent — the client just asks the agent to sign things.

Your SSH client  ──── "sign this challenge" ────►  ssh-agent
                 ◄─── "here's the signature" ─────  ssh-agent
Enter fullscreen mode Exit fullscreen mode

The agent communicates through a Unix socket, stored at a path like /tmp/ssh-agent.XXXXX/agent.12345. The environment variable SSH_AUTH_SOCK points to this socket.

What Forwarding Does

When you SSH to a remote server with ForwardAgent yes, SSH does something specific: it creates a new socket on the remote server that tunnels back to your local agent.

Remote server /tmp/ssh-XXXXX/agent.5678  ──► tunnel ──►  Your local ssh-agent
Enter fullscreen mode Exit fullscreen mode

On the remote server, SSH_AUTH_SOCK points to this forwarded socket. Any process on the remote server that connects to this socket gets routed back to your local agent.

This means: from the remote server's perspective, your local agent is present and available. When you run ssh db.internal from the bastion, the bastion's SSH client uses the forwarded socket, which calls back to your local agent, which signs the challenge with your private key.

It feels like your key is on the bastion. It effectively is — for the duration of your session.

The Attack

Here's the problem. Anyone with root access on the remote server can:

  1. Find all active forwarded agent sockets: ls /tmp/ssh-*/
  2. Impersonate any user with a forwarded socket by setting SSH_AUTH_SOCK to their socket path
  3. Use that socket to authenticate as that user to any server their key unlocks
# An attacker with root on your bastion runs:
ls /tmp/ssh-*/
# Finds: /tmp/ssh-AbCdEf/agent.1234

SSH_AUTH_SOCK=/tmp/ssh-AbCdEf/agent.1234 ssh ubuntu@db.internal
# Connected. As you. Using your key.
Enter fullscreen mode Exit fullscreen mode

The attacker never sees your private key. They don't need to. They use your agent — which does the signing for them.

This works for every server your key can reach, for as long as your SSH session to the bastion remains open. If you leave a session running overnight, that window stays open overnight.

What Makes This Worse

  • It requires root on the compromised server. Root access is more common than people expect: a container escape, a sudo misconfiguration, an unpatched privilege escalation vulnerability.
  • It's invisible. Your agent doesn't log that it was used. You get no notification.
  • It's transitive. If you forward your agent to server A, and server A forwards it to server B, anyone with root on either server can use your agent.
  • It affects all keys in your agent. Not just the key you used to connect — every key loaded in ssh-agent.

ProxyJump: The Right Solution

ProxyJump was designed specifically for multi-hop SSH. It solves the "connect through a bastion" problem without forwarding your agent.

How ProxyJump Works

Instead of giving the bastion access to your agent, ProxyJump tells your local SSH client to make two connections — both originating from your machine:

Your machine  ──TCP──►  Bastion:22  ──TCP tunnel──►  db.internal:22
              (full SSH)             (proxied through bastion TCP)
Enter fullscreen mode Exit fullscreen mode

Your SSH client connects to the bastion and immediately asks the bastion to open a TCP connection to the target server. The bastion acts as a dumb TCP proxy — it passes bytes back and forth but has no involvement in the authentication. Your local SSH client handles authentication to both the bastion and the target.

The critical difference: your agent never touches the bastion. The bastion's SSH server forwards TCP traffic; it never performs any authentication on your behalf.

Implementation

Host bastion
    HostName bastion.example.com
    User ubuntu
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    # Note: No ForwardAgent here

Host db.internal
    User ubuntu
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    ProxyJump bastion
Enter fullscreen mode Exit fullscreen mode

That's it. ssh db.internal connects through the bastion, authenticates to both with your local agent, and never forwards the agent to either server.

Multi-Hop

Host deep.internal
    ProxyJump bastion,internal-gateway.example.com
Enter fullscreen mode Exit fullscreen mode

Each hop is a TCP proxy. Your local SSH client authenticates to each server in sequence. No agent is forwarded anywhere in the chain.

Files and Commands

ProxyJump works seamlessly with all SSH-based tools:

scp -J bastion file.txt ubuntu@db.internal:/tmp/
rsync -avz -e "ssh -J bastion" local/ ubuntu@db.internal:remote/
sftp -J bastion ubuntu@db.internal
Enter fullscreen mode Exit fullscreen mode

Side-by-Side Comparison

Agent Forwarding ProxyJump
Mechanism Forwards agent socket to remote TCP proxy through intermediate host
Authentication location Remote server calls back to local agent Local client authenticates everywhere
Agent exposure Agent available on remote server Agent never leaves local machine
Root attack surface Root on bastion can use your agent Root on bastion sees TCP bytes only
Session window Risk exists for entire session duration No persistent risk
Multi-hop Requires forwarding through each hop Handled natively, comma-separated
SCP/rsync support Works (but risk applies) Works, no risk
Available since OpenSSH 1.x OpenSSH 7.3 (2016)

When Agent Forwarding Might Be Acceptable

Agent forwarding isn't always wrong. There are narrow cases where it's appropriate:

Interactive development servers you fully control
If you're the only user on a server, you own root, and it exists solely as your dev environment — forwarding your agent there is low risk. The threat model requires an attacker to have root on that server; if you are root and the server isn't shared, that threat is remote.

Legacy systems that don't support ProxyJump
OpenSSH 7.3+ supports ProxyJump. Systems running older versions may need ProxyCommand or agent forwarding as a workaround. This should be a migration trigger, not a permanent state.

When you explicitly need the agent on the remote machine
Legitimate use cases: cloning private Git repos on a remote server without copying keys, or running SSH commands from a remote machine that genuinely need to reach a third server you can't reach directly. Even here, minimize the exposure window (connect, do the operation, disconnect).

Mitigations If You Must Use Agent Forwarding

If you genuinely need it, minimize the risk:

# Load only the specific key needed, with a time limit
ssh-add -t 3600 ~/.ssh/id_ed25519_specific   # Expires in 1 hour

# Confirm what's in your agent before forwarding
ssh-add -l

# Remove all keys after you're done
ssh-add -D
Enter fullscreen mode Exit fullscreen mode

Use ssh-add -c to require confirmation for every agent operation:

ssh-add -c ~/.ssh/id_ed25519
Enter fullscreen mode Exit fullscreen mode

Every time the forwarded socket is used, you'll get a local prompt asking you to confirm. An attacker exploiting your forwarded agent would trigger this prompt — though if you're away from your machine, you might confirm it without thinking.

Lock down the forwarding to the minimum scope:

# Only forward to a specific, trusted host — not everything
Host trusted-dev-server
    HostName 10.0.0.50
    ForwardAgent yes

Host *
    ForwardAgent no   # Default: off
Enter fullscreen mode Exit fullscreen mode

Auditing and Detecting Agent Forwarding

Find Active Forwarded Sockets

On any server you're connected to:

# Your own forwarded socket
echo $SSH_AUTH_SOCK

# All agent sockets (if you have permission)
ls /tmp/ssh-*/

# As root: find all agent sockets across all users
find /tmp -name "agent.*" -type s 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

Check Who's Using Your Agent

There's no built-in logging for agent use, but you can add it with ssh-agent wrappers or tools like ssh-audit-agent. A simpler approach: use hardware security keys (YubiKey, etc.) which require physical touch for each signing operation — a forwarded socket can't silently use the key without your physical presence.

On the Server: Restrict Agent Forwarding

If you administer the bastion or any intermediate server, disable forwarding for users who don't need it:

# /etc/ssh/sshd_config
AllowAgentForwarding no
Enter fullscreen mode Exit fullscreen mode

This is a global disable. If ProxyJump is your standard access pattern, there's no reason for the bastion to accept agent forwarding at all.


Migrating an Existing Setup

If your team currently uses agent forwarding for bastion access, migration to ProxyJump is straightforward and backward-compatible.

Step 1: Add ProxyJump to ~/.ssh/config

Host bastion
    HostName bastion.example.com
    User ubuntu
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    ForwardAgent no          # Explicitly disable

Host *.internal
    ProxyJump bastion
    User ubuntu
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
Enter fullscreen mode Exit fullscreen mode

Step 2: Verify Target Server Keys Are in known_hosts

With agent forwarding, the bastion performed the second-hop authentication, so only the bastion's key was in your local known_hosts. With ProxyJump, your local client authenticates to every hop, so every server's key needs to be in your local known_hosts.

# Add target server keys through the proxy
ssh-keyscan -J bastion db.internal >> ~/.ssh/known_hosts
Enter fullscreen mode Exit fullscreen mode

Step 3: Disable Agent Forwarding on the Bastion

# /etc/ssh/sshd_config on bastion
AllowAgentForwarding no
Enter fullscreen mode Exit fullscreen mode
sudo systemctl restart ssh
Enter fullscreen mode Exit fullscreen mode

Step 4: Verify

# Connect to target — should work without agent forwarding
ssh db.internal

# Verify agent socket is not present on bastion
ssh bastion "echo \$SSH_AUTH_SOCK"
# Should be empty
Enter fullscreen mode Exit fullscreen mode

The Deeper Principle

The agent forwarding problem illustrates a broader security principle: convenience and security are often in tension, and convenience has a way of winning by default.

Agent forwarding feels seamless precisely because it's transparent. You never see the forwarded socket. You never see it being used. The attack would also be transparent — the attacker uses your agent, achieves their goal, and you see nothing.

ProxyJump is equally transparent from a user experience perspective. ssh db.internal works the same way. The difference is what's happening underneath — and what an attacker on an intermediate server can and cannot access.

When two solutions have the same user-facing behavior and one has a material security advantage, the choice should be obvious. The only reason agent forwarding persists is inertia: it's what tutorials written before 2017 recommended, and those tutorials are still being followed today.


Quick Reference

Replace this:

Host bastion
    ForwardAgent yes
Enter fullscreen mode Exit fullscreen mode

With this:

Host bastion
    ForwardAgent no

Host *.internal
    ProxyJump bastion
Enter fullscreen mode Exit fullscreen mode

And on the bastion server:

# /etc/ssh/sshd_config
AllowAgentForwarding no
Enter fullscreen mode Exit fullscreen mode

That's the migration. It takes five minutes and eliminates an entire category of attack surface.


Follow for more SSH security content. Next: ControlMaster and SSH connection multiplexing for faster deployments.

Top comments (0)