How to Use an SSH Jump Host with ProxyJump

By 

•

Published on

•

11 min read

Laptop connected through an SSH jump host to two private servers

When a server is on a private network, you may need to connect through another machine before you can reach it. This intermediate server is called a jump host or bastion host. You could log in to the bastion and run ssh again from there, but using keys for the second connection would require a private key on the bastion or agent forwarding.

OpenSSH solves this with ProxyJump. You tell your local client which jump host to go through, and it connects to the internal server in one command. Your private keys stay on your own machine, and the session to the internal server is encrypted end to end.

This guide explains how to connect through a jump host with ssh -J, save the route in ~/.ssh/config, copy files and forward ports through the bastion, and restrict a jump account so it can do nothing except relay connections.

How ProxyJump Works

When you connect with ProxyJump, your client first opens an SSH connection to the jump host. It then asks the jump host to open a TCP connection to the internal server, usually on port 22, and starts a second SSH connection through it.

Both SSH connections start on your computer. You authenticate to each server separately, and the bastion relays the encrypted connection to the internal server. You do not need to copy private keys to the bastion or enable ForwardAgent, which would let someone with root access on the bastion use your forwarded agent to authenticate elsewhere while it is available.

The examples in this guide use a jump host at bastion.example.com and an internal server at 10.0.0.50. Replace these addresses and the admin username with your own. Your computer must be able to reach the bastion, and the bastion must be able to reach the internal server’s SSH port.

We will use public key authentication, so your public key must be authorized for the account you use on each machine. You can use a different key for each server. See how to generate SSH keys if you have not set them up. ProxyJump also works with password authentication when the servers allow it.

Connect with ssh -J

The -J option takes the jump host as its argument, followed by the destination:

txt
ssh -J [user@]jumphost[:port] [user@]destination

To log in to the internal server as admin through the bastion, run:

Terminal
ssh -J admin@bastion.example.com admin@10.0.0.50

The first time you connect, SSH asks you to confirm the host key of each machine. Verify both fingerprints with the server administrator or through a trusted console before accepting them. After authentication, you get a shell on 10.0.0.50.

The bastion opens the TCP connection to 10.0.0.50, so your computer does not need a route to that private address. If you use an internal hostname instead, it is normally resolved on the bastion.

If the jump host listens on a non-standard port, add it after a colon:

Terminal
ssh -J admin@bastion.example.com:2222 admin@10.0.0.50

For the destination port, use the normal -p option, which applies only to the final host.

Chain Multiple Jump Hosts

Some networks put more than one bastion in front of the servers, for example an internet-facing jump host and a second one inside a management network. Separate the jump hosts with commas, in the order the connection should pass through them:

Terminal
ssh -J admin@bastion.example.com,admin@10.10.0.5 admin@10.20.0.80

SSH connects to bastion.example.com, from there to 10.10.0.5, and from there to the final server. Each hop is still authenticated from your machine.

Options Do Not Apply to the Jump Host

Options such as -i for a key file and most -o settings apply to the destination, not the jump host. If the bastion needs a particular key, put its settings in ~/.ssh/config. SSH reads the configuration file for each hop. Otherwise, the jump connection may fail with Permission denied (publickey) even though the same -i option works when you connect to the bastion directly.

Configure ProxyJump in ~/.ssh/config

The SSH config file lets you save the route under a short name. Open the file on your local computer:

Terminal
nano ~/.ssh/config

Define the bastion, then reference it from the internal host with ProxyJump. This example uses a separate key for each server:

~/.ssh/configini
Host bastion
    HostName bastion.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519_bastion
    IdentitiesOnly yes

Host db1
    HostName 10.0.0.50
    User admin
    IdentityFile ~/.ssh/id_ed25519_internal
    IdentitiesOnly yes
    ProxyJump bastion

Set each IdentityFile to an existing private key on your computer. If you use the same key for both accounts, use the same path in both blocks. IdentitiesOnly yes keeps SSH from trying unrelated keys offered by your agent.

ProxyJump bastion refers to the Host bastion block, so the jump connection uses its hostname, user, and key. The db1 block supplies the settings for the internal server. You can now connect with:

Terminal
ssh db1

To check which jump host SSH will use for a name, print the effective configuration with ssh -G:

Terminal
ssh -G db1 | grep -i proxyjump
output
proxyjump bastion

The output confirms that db1 uses the bastion alias. ssh -G only prints the client settings; it does not connect to either server.

Use a Wildcard for Private Addresses

When every server on a private network sits behind the same bastion, a wildcard pattern saves you from writing a block for each one:

~/.ssh/configini
Host 10.0.0.*
    User admin
    IdentityFile ~/.ssh/id_ed25519_internal
    IdentitiesOnly yes
    ProxyJump bastion

Now ssh 10.0.0.50 and ssh 10.0.0.51 go through the bastion automatically. The pattern matches the address you type in the command. It does not match the db1 alias just because its HostName is 10.0.0.50, so keep ProxyJump bastion in that alias’s block.

Place more specific Host blocks above wildcard ones, because SSH uses the first value it finds for most options.

If one host matches the wildcard but should be reached directly, override it with ProxyJump none in a block above the wildcard:

~/.ssh/configini
Host 10.0.0.1
    ProxyJump none

Copy Files Through the Jump Host

scp and sftp accept the same -J option as ssh. To upload a local file through the bastion, run:

Terminal
scp -J admin@bastion.example.com backup.sql admin@10.0.0.50:/tmp/

Replace backup.sql with your local filename. When the route is in ~/.ssh/config, you do not need -J. scp , sftp, and rsync over SSH use the same host settings:

Terminal
scp backup.sql db1:/tmp/
sftp db1
rsync -avz ./site/ db1:/var/www/site/

The destination directory must be writable by your remote user. rsync must be installed on your computer and the internal server; it does not need to be installed on the bastion.

For rsync without a config entry, pass the jump host through the remote shell option:

Terminal
rsync -avz -e "ssh -J admin@bastion.example.com" ./site/ admin@10.0.0.50:/var/www/site/

For more transfer options, see the guide on using rsync over SSH .

Forward a Port Through the Jump Host

ProxyJump combines with SSH port forwarding . Suppose a PostgreSQL server on db1 listens on 127.0.0.1:5432. The following command makes it available on port 15432 of your computer:

Terminal
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:15432:127.0.0.1:5432 db1

-L forwards 127.0.0.1:15432 on your computer to 127.0.0.1:5432 as seen from db1. Binding to 127.0.0.1 keeps the local port accessible only from your computer. Using 15432 also avoids a conflict if you already run PostgreSQL locally on port 5432.

-N tells SSH not to start a remote shell. ExitOnForwardFailure=yes makes SSH exit if it cannot create the listener, for example because port 15432 is already in use. It does not check whether PostgreSQL is reachable.

Because db1 has ProxyJump bastion in the config file, the tunnel passes through the bastion without any extra options. The SSH server on db1 must also allow local TCP forwarding. Connect your database client to 127.0.0.1:15432 and press Ctrl+C to close the tunnel when you are done.

Use ProxyCommand on Older Clients

ProxyJump and -J were added in OpenSSH 7.3. If you have to work from an older client, use ProxyCommand with ssh -W, which does the same thing in a more verbose way:

~/.ssh/configini
Host db1
    HostName 10.0.0.50
    User admin
    ProxyCommand ssh -W %h:%p bastion

%h and %p expand to the hostname and port of db1, and -W tells the bastion connection to forward standard input and output to that address. This example uses the bastion alias defined earlier. Replace the existing ProxyJump line when trying ProxyCommand; the two options compete, and whichever is set first takes effect. On current clients, use ProxyJump.

Lock Down the Jump Host

A dedicated jump account does not need to run commands on the bastion. The following example assumes you have already created a jump user and installed your public key in that user’s ~/.ssh/authorized_keys. Use a separate account for administering the bastion.

Before changing the configuration, confirm that you can authenticate as jump with your key. Then, from your administrative account on the bastion, open the SSH server configuration:

Terminal
sudo nano /etc/ssh/sshd_config

Add the following block at the end of the file, after the global settings and any existing Match blocks. List the internal servers the account may reach in PermitOpen:

/etc/ssh/sshd_configini
Match User jump
    AuthenticationMethods publickey
    AllowTcpForwarding local
    PermitOpen 10.0.0.50:22 10.0.0.51:22
    AllowStreamLocalForwarding no
    X11Forwarding no
    AllowAgentForwarding no
    PermitTunnel no
    MaxSessions 0

Here is what each setting does:

  • AuthenticationMethods publickey - Requires public key authentication for this account.
  • AllowTcpForwarding local - Allows the forwarding that ProxyJump needs and blocks remote forwarding (-R) from the bastion.
  • PermitOpen - Limits TCP forwarding to the listed hosts and ports. SSH compares the destination literally, so use the IP address or hostname sent by the client after any HostName substitution.
  • AllowStreamLocalForwarding no - Disables Unix socket forwarding, which PermitOpen does not restrict.
  • X11Forwarding no and AllowAgentForwarding no - Disable forwarding that a relay account never needs.
  • PermitTunnel no - Disables tunnel-device forwarding.
  • MaxSessions 0 - Prevents shell, command, and subsystem sessions, including SFTP on the bastion, while still allowing TCP forwarding. ProxyJump does not request a session on the bastion, so it keeps working.

The Match User jump block applies only to the jump account. Earlier matching blocks may take precedence for the same settings, so check the effective configuration as well as the file syntax.

Warning
Keep your administrative SSH session open while changing these settings. Confirm that the jump account can still reach the internal server in a new connection before you log out.

Check the syntax before applying the change, so a typo does not break SSH access to the bastion:

Terminal
sudo sshd -t

If the command prints nothing, reload the service:

Terminal
sudo systemctl reload ssh

On Fedora and RHEL, use sudo systemctl reload sshd instead. From your computer, test a connection through the restricted account:

Terminal
ssh -J jump@bastion db1

You should get a shell on the internal server. A direct shell or SFTP connection to the bastion as jump should fail. If you use the bastion alias, change its User setting to jump so that ssh db1 uses the restricted account too.

Troubleshooting

“channel 0: open failed: administratively prohibited: open failed”
The jump host refused to open the connection to the next hop. The full error looks like this:

output
channel 0: open failed: administratively prohibited: open failed
stdio forwarding failed
Connection closed by UNKNOWN port 65535

Check AllowTcpForwarding, DisableForwarding, and PermitOpen on the bastion. The key entry in authorized_keys can also block forwarding with options such as restrict or no-port-forwarding.

To inspect the effective server settings, run this command on the bastion. Replace 192.0.2.10 and client.example.com with the client address and hostname as seen by the bastion, and use the account you connect with:

Terminal
sudo sshd -T -C user=jump,host=client.example.com,addr=192.0.2.10 | grep -E '^(allowtcpforwarding|disableforwarding|permitopen|maxsessions) '

For the restricted account above, expect allowtcpforwarding local, disableforwarding no, maxsessions 0, and the two allowed destinations in permitopen. A key-level restriction still applies even when these server settings allow forwarding.

“Host key verification failed” on the jump host
SSH could not verify the bastion’s host key. Connect to the bastion directly and compare the displayed fingerprint with one obtained from the server administrator or a trusted console. If the key has changed, verify the reason before updating known_hosts. Put any per-host settings in the bastion’s Host block; command-line settings for the destination generally do not apply to the jump connection.

“Permission denied (publickey)” on the internal server
The jump worked, but the final server rejected your key. Check the destination username and IdentityFile, and confirm that the corresponding public key is authorized on the internal server. The SSH public key troubleshooting guide covers key permissions and other common causes.

The connection hangs before the internal server responds
Run ssh -v db1 to see which hop stalls. If the connection to the bastion succeeds, check the internal address, SSH port, and firewall rules between the two servers. From a separate administrative account on the bastion, you can test TCP connectivity with nc -zv 10.0.0.50 22. The restricted jump account cannot run that command.

Conclusion

Save the route and per-host keys in ~/.ssh/config so you can use the same alias for SSH sessions, file transfers, and port forwarding. For more client options, see the ssh command guide .

Tags

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page