The -J flag in SSH is a surprisingly powerful way to chain connections, letting you hop through one or more intermediate servers (jump hosts or bastions) to reach your final destination, all with a single command.

Let’s see it in action. Imagine you have a secure internal network where your target server internal-server.example.com (IP 192.168.1.100) is only accessible from a bastion host bastion.example.com (IP 10.0.0.5). You have SSH access to both.

Here’s how you’d connect directly to internal-server.example.com as user deploy from your local machine:

ssh deploy@internal-server.example.com -J deploy@bastion.example.com

When you run this, your local SSH client first establishes a connection to bastion.example.com. Once that connection is secure, it then initiates a second SSH connection from the bastion host to internal-server.example.com. Your local terminal is then multiplexed over both connections, making it appear as if you’re directly connected to internal-server.example.com.

This solves a common security problem: restricting direct SSH access to sensitive internal servers. Instead of opening up SSH ports on every machine, you can lock down a single bastion host, which acts as the sole entry point. All traffic to the internal network then funnels through this hardened gateway.

Internally, the -J flag uses SSH’s ProxyCommand feature under the hood. When you specify -J user@host, SSH effectively configures a ProxyCommand that looks something like this (simplified):

ProxyCommand ssh -W %h:%p user@host

The -W %h:%p part tells the first SSH client (connecting to the bastion) to forward its standard input and output (-W) to the host (%h) and port (%p) of the final destination. So, the bastion becomes a simple tunnel.

You can chain multiple jump hosts by separating them with commas:

ssh deploy@internal-server.example.com -J user1@jump1.example.com,user2@jump2.example.com

This will connect to jump1.example.com, then from jump1.example.com to jump2.example.com, and finally from jump2.example.com to internal-server.example.com.

Configuration can also be done in your ~/.ssh/config file. This is cleaner for frequently used jump hosts.

Host bastion
    Hostname bastion.example.com
    User user1
    Port 22

Host internal
    Hostname internal-server.example.com
    User deploy
    ProxyJump bastion

With this config, you can simply run:

ssh internal

And it will automatically use bastion as the jump host.

The -J flag handles authentication for each hop independently. If your keys are set up correctly for both the bastion and the internal server, it will be seamless. You might be prompted for passwords or passphrases for each jump if keys aren’t configured.

One thing most people don’t realize is that the -J flag is not just for SSH connections. When you use it, SSH is establishing a raw TCP tunnel using ProxyCommand’s -W option. This means you can tunnel any TCP-based protocol through it, not just SSH itself. For instance, you could forward a database connection or a web server port this way, though you’d typically use -L or -D for those specific use cases. The -J flag is primarily syntactic sugar for setting up that ProxyCommand tunnel to an SSH server.

The next logical step after mastering jump hosts is understanding how to automate SSH key distribution and management across multiple servers using tools like Ansible or ssh-copy-id in conjunction with jump hosts.

Want structured learning?

Take the full Ssh course →