How to use ProxyJump and a jump host in SSH

Run ssh -J user@bastion user@target to reach a private server through a jump host. In ~/.ssh/config, set ProxyJump bastion under the target's Host block. Your SSH session stays encrypted from your machine to the target, your private key stays on your machine, and the jump host only passes bytes along. It needs OpenSSH 7.3 or later on the client.

Updated . Run with OpenSSH 10.3 on macOS through OpenSSH 10.0 jump hosts on Debian 13, to private targets with no published port: -J, ProxyJump, two hops, ProxyCommand, the restricted jump user, AllowTcpForwarding no, and the listing of forwarded keys with ssh-add -l. scp -J, sftp -J and rsync through a jump were run with OpenSSH 10.0 on Debian 13.

On the command line

ssh -J user@bastion user@target
your machine -> SSH to bastion -> TCP from bastion to target -> SSH from your machine to target, inside it

The jump host needs a reachable address. The target does not. Its name is resolved by the jump host, so a private name or a private address such as 10.0.0.5 works even when your machine cannot resolve or reach it. A custom port goes after a colon: -J user@bastion:2222.

scp and sftp take the same option, and rsync passes it to ssh:

scp -J user@bastion file.txt user@target:/tmp/
sftp -J user@bastion user@target
rsync -av -e "ssh -J user@bastion" ./site/ user@target:/var/www/site/

In your ssh config

Host bastion
  HostName 203.0.113.10
  User user
  IdentityFile ~/.ssh/id_ed25519

Host target
  HostName 10.0.0.5
  User user
  ProxyJump bastion
  IdentityFile ~/.ssh/id_ed25519

Now ssh target is all you type, and scp, sftp and rsync work the same way. To apply it to a whole network:

Host 10.0.*
  ProxyJump bastion

Do not write a pattern that matches the jump host itself. To skip the jump for one connection, use ssh -o ProxyJump=none target.

For more on Host blocks, patterns and Match, see ssh config file examples.

Multiple hops

List the jump hosts in order, separated by commas:

ssh -J user@bastion,user@inner user@target
Host target
  ProxyJump bastion,inner

Each name in the list after the first is resolved from the hop before it.

Command-line options do not reach the jump host

This is the most common surprise. -i and -o apply only to the final target, because the connection to the jump host is a separate ssh process that reads only your config. We tested it: with the key named only by -i, login to the jump host failed, and the message was just this:

Connection closed by UNKNOWN port 65535

So define the jump host's user, port and key in a Host block of your config. To see each step, use ssh -v target. It prints the implied command, and shows authentication to the jump host and then to the target.

Why it is safer than agent forwarding

The older way was to log in to the jump host with ssh -A, and run ssh again from there. That leaves a socket to your agent on the jump host while you are connected. In our test, a command on the jump host could list the keys of the laptop's agent:

ssh -A user@bastion 'ssh-add -l'

Anyone with root on that jump host can use that socket to log in as you, while you are connected. They cannot copy the key, but they do not need to.

With ProxyJump, nothing of yours is on the jump host:

  • Your private key and agent stay on your machine, and the jump host never has access to them.
  • Your machine does the login to the target and checks its host key against your own known_hosts.
  • The jump host sees encrypted bytes, and knows which host and port you connected to, and when. It cannot read the session.

A compromised jump host can still block you or watch that traffic pattern, and it can attack the network it sits on. Keep it small and up to date. See the risks of agent forwarding.

Old versions: ProxyCommand with -W

For clients older than OpenSSH 7.3, use ssh -W, which has existed since version 5.4:

Host target
  HostName 10.0.0.5
  ProxyCommand ssh -W %h:%p bastion

Use one of the two options for a host, not both. The manual says they compete, and whichever comes first prevents the other from taking effect. ProxyCommand is also useful when you need to pass your own options to the inner ssh, such as -i.

What the jump host must allow

Jumping uses a TCP forward on the jump host, so it needs forwarding on. With AllowTcpForwarding no you see:

stdio forwarding failed
Connection closed by UNKNOWN port 65535

You can give jump users nothing beyond this. In the jump host's sshd configuration:

Match User jump
  PermitTTY no
  X11Forwarding no
  AllowAgentForwarding no
  AllowTcpForwarding local
  PermitOpen 10.0.0.5:22 10.0.0.6:22
  ForceCommand /usr/sbin/nologin

In our test, a login through this user reached the listed target, a host not on the list failed with stdio forwarding failed, and a normal login printed This account is currently not available. PermitOpen compares text exactly and does no lookups, so its entries must match the name or address the client asks for.

The target's firewall should accept SSH only from the jump host, or the jump adds little protection.

Questions

What is the difference between ProxyJump and agent forwarding?

With ProxyJump your key and agent stay on your machine, and the jump host only passes encrypted bytes. Agent forwarding leaves a socket on the jump host that root there can use while you are connected.

Can I use more than one jump host?

Yes. List them in order, separated by commas: ssh -J user@bastion,user@inner user@target, or ProxyJump bastion,inner in your config.

How do I use scp or rsync through a jump host?

scp and sftp take the same option: scp -J user@bastion file.txt user@target:/tmp/. For rsync, pass it to ssh with -e "ssh -J user@bastion". With ProxyJump in your config, plain scp target:... works too.

Why does -i not work for the jump host?

Command-line options apply only to the final target. Set the jump host's user, port and key in its own Host block.

Sources

More in Tunnels and jump hosts

All guides