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.