SSH ProxyJump: a practical guide to jump hosts
Reach servers behind a bastion with ssh -J and ProxyJump: config examples, multi-hop chains, scp and rsync through a jump host, and bastion hardening.
On this page
The one-line versionPut it in ~/.ssh/configMore than one hopHow it works, and why it is safeProxyJump vs ProxyCommandCopy files through a jump hostTunnels through the chainMake repeated connections fastLock down the bastionWhen it failsJump chains in Gatesys SSHFAQA jump host, or bastion, is the one machine in a network that accepts SSH from outside. Everything else is reached through it. OpenSSH has had a clean way to do this since version 7.3, released in 2016: ProxyJump. This guide covers the command, the config file, chains of several hops, file copies through a bastion, and the settings that keep the bastion itself safe.
The one-line version
ssh -J alice@bastion.example.com deploy@10.0.3.4-J names the jump host. Your client connects to bastion.example.com, asks it to open a TCP connection to 10.0.3.4 port 22, and then runs a second, complete SSH session to the target through that connection.
A port or a user for the bastion goes in the same argument:
ssh -J alice@bastion.example.com:2222 deploy@10.0.3.4Put it in ~/.ssh/config
Typing -J every time gets old. In ~/.ssh/config, the same thing is one keyword:
Host bastion
HostName bastion.example.com
User alice
IdentityFile ~/.ssh/id_ed25519
Host db-primary
HostName 10.0.3.4
User deploy
ProxyJump bastionNow ssh db-primary does the right thing, and so does everything built on ssh: scp, sftp, rsync, git and Ansible all read the same file.
A pattern covers a whole network at once:
Host 10.0.3.* *.internal.example.com
ProxyJump bastionMore than one hop
List the hops in order, separated by commas:
ssh -J bastion,inner-bastion deploy@db-primaryIn the config file there are two ways to write a chain, and the second is easier to maintain:
# Every hop on the target
Host db-primary
ProxyJump bastion,inner-bastion
# Or: each host knows only the hop in front of it
Host inner-bastion
ProxyJump bastion
Host db-primary
ProxyJump inner-bastionWith the second form, a change to how inner-bastion is reached is made in one place, and every host behind it follows.
How it works, and why it is safe
ProxyJump uses SSH’s own TCP forwarding. The client asks the bastion for a direct-tcpip channel to the target, then speaks SSH to the target inside that channel. Three things follow:
- The bastion cannot read your session. The connection to the target is encrypted end to end between your computer and the target. The bastion forwards bytes it cannot decrypt.
- Your private key never leaves your computer. Each hop is authenticated from your machine. Nothing is copied to the bastion and no agent is forwarded.
- The bastion needs nothing extra. No
nc, no shell access even, only an SSH server that allows forwarding.
That second point is the reason to prefer ProxyJump to the old habit of ssh -A to a bastion and then ssh again from there. Agent forwarding lets anyone with root on the bastion use your agent for as long as you are connected. ProxyJump gives them nothing to use.
ProxyJump vs ProxyCommand
Before 7.3, the idiom was:
Host db-primary
ProxyCommand ssh -W %h:%p bastionProxyJump bastion is shorthand for exactly this. ProxyCommand is still useful when the proxy is not SSH at all: a cloud provider’s session tool, an HTTP proxy with nc -X connect, or cloudflared. If the thing in the middle is an SSH server, use ProxyJump.
Copy files through a jump host
With ProxyJump in the config file, nothing changes:
scp backup.sql db-primary:/tmp/
rsync -av ./site/ web-1:/srv/site/Without a config entry, pass the option:
scp -o ProxyJump=bastion backup.sql deploy@10.0.3.4:/tmp/
rsync -av -e "ssh -J bastion" ./site/ deploy@10.0.3.4:/srv/site/For Ansible, set it on the group:
ansible_ssh_common_args: "-o ProxyJump=alice@bastion.example.com"Tunnels through the chain
Port forwarding rides the same chain. To reach PostgreSQL on a database that only the bastion can see:
ssh -J bastion -L 15432:localhost:5432 deploy@db-primaryThen point your client at localhost:15432. The forward ends on db-primary, not on the bastion. More in SSH port forwarding explained.
Make repeated connections fast
Every new ssh through a chain repeats every handshake. Connection sharing reuses the first:
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%C
ControlPersist 10mThe second ssh, scp or git push to the same host then opens in a fraction of a second.
Lock down the bastion
A bastion’s job is to forward, so let its users do nothing else. In the bastion’s /etc/ssh/sshd_config:
Match Group jumpers
AllowTcpForwarding yes
PermitOpen 10.0.3.4:22 10.0.3.5:22
AllowAgentForwarding no
X11Forwarding no
PermitTTY no
ForceCommand /usr/sbin/nologinPermitOpenlimits where a forwarded connection may go. It takeshost:portpairs, and*stands for any host or any port.PermitTTY noandForceCommanddeny a shell.ProxyJumpstill works, because it never asks for one.- Keys only:
PasswordAuthentication no.
Before you save a change like this on a machine you reach over SSH, read how to edit sshd_config without locking yourself out.
When it fails
A chain can break at any hop, and OpenSSH reports it in one line. The usual suspects:
| Message | Meaning |
|---|---|
channel 0: open failed: administratively prohibited | The bastion does not allow forwarding, or PermitOpen excludes the target |
channel 0: open failed: connect failed: Connection refused | The bastion reached the target, and nothing listens on that port |
channel 0: open failed: connect failed: Name or service not known | The bastion cannot resolve the target’s name |
Permission denied (publickey) | A login failed. Read the user@host in front of it to see which hop |
There is a full walk-through in Debugging a failed ProxyJump chain, hop by hop.
Jump chains in Gatesys SSH
Gatesys SSH builds chains the same way OpenSSH does, with direct-tcpip channels, so a bastion that works with ssh -J works with it. What it adds:
- Import. File › Import from ~/.ssh/config… turns
ProxyJumplines, comma-separated chains included, into saved jump chains. - One connection per host. The terminal, the SFTP browser, tunnels and metrics share one session, so the chain is negotiated once.
- A diagnosis when it breaks. Hop Doctor names the hop and the step, and never logs in a second time to find out.
All of it is in the free plan, with no account.
FAQ
What is the difference between ProxyJump and ProxyCommand?
ProxyJump host is shorthand for ProxyCommand ssh -W %h:%p host. Use ProxyJump when the thing in the middle is an SSH server, and ProxyCommand when it is some other kind of proxy.
Can the jump host read my session?
No. Your client runs a separate, end-to-end encrypted SSH session to the target through the jump host. The jump host sees the destination address and the volume of traffic, not its content.
Do I need my private key on the jump host?
No, and you should not put it there. Every hop is authenticated from your own computer, using your local key or agent.
Does ProxyJump need netcat on the bastion?
No. It uses the SSH server’s built-in forwarding. Only very old setups with ProxyCommand ssh bastion nc %h %p needed nc.
Which OpenSSH version do I need?
ProxyJump and -J arrived in OpenSSH 7.3. Only the client needs it. The servers just need to allow TCP forwarding.
