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
- ssh(1), OpenBSD manual
- ssh_config(5), OpenBSD manual
- OpenSSH 7.3 release notes, ProxyJump
- OpenSSH 5.4 release notes, ssh -W