← Back to Blog

SSH Jump Hosts & ProxyJump Configuration: The Complete Guide

Managing infrastructure securely often involves placing critical servers—like databases, application backends, and internal APIs—inside a private network, inaccessible directly from the public internet. To reach these isolated resources, developers and sysadmins rely on an SSH Jump Host (often called an SSH bastion host). While manually configuring these multi-hop SSH connections can be tedious and prone to syntax errors, using a privacy-first, purely local SSH config generator ensures you build robust configurations without ever uploading your server IPs to a third party.

ZeroData's core philosophy centers on exactly this: no uploads, purely local processing right within your browser. Securing your infrastructure begins with keeping your topology private.

Whether you are setting up a single SSH jump host to reach a private VPC, or configuring complex multi-hop SSH routing across multiple DMZs (Demilitarized Zones), understanding the nuances of the ProxyJump directive is essential. In 2026, SSH client configurations have evolved significantly, rendering older patterns like ProxyCommand obsolete and introducing streamlined, secure-by-default options. In this guide, we will explore everything from basic single-hop bastion connections to advanced multi-hop routing, firewall troubleshooting, and why maintaining client-side generation for your configs is the ultimate security best practice.

TL;DR: The Quick Solution

To connect to a private server through a bastion host directly from the command line:

ssh -J [email protected] [email protected]

To persist this in your ~/.ssh/config:

Host target
    HostName 10.0.0.50
    User admin
    ProxyJump bastion-host.com

1. The Anatomy of an SSH Bastion Host & DMZ Patterns

In enterprise network architecture, a DMZ (Demilitarized Zone) is a physical or logical subnetwork that contains and exposes an organization's external-facing services to an untrusted network, typically the internet. The purpose of a DMZ is to add an additional layer of security to an organization's local area network (LAN); an external network node can access only what is exposed in the DMZ.

An SSH Bastion Host is a special-purpose server located within this DMZ. It is designed and configured to withstand attacks. The bastion host is the only server in your infrastructure that accepts SSH connections from the outside world. All other servers (databases, application servers, internal tools) reside in a private subnet and only accept SSH connections originating from the bastion host.

┌─────────────────┐       ┌─────────────────┐       ┌─────────────────┐
│                 │       │                 │       │                 │
│  Local Machine  ├──────►│ SSH Jump Host   ├──────►│  Target Server  │
│  (SSH Client)   │       │ (Bastion / DMZ) │       │ (Private Subnet)│
│                 │       │                 │       │                 │
└─────────────────┘       └─────────────────┘       └─────────────────┘
      Public IP                 Public IP                 Private IP
                                203.0.113.1               10.0.5.42

This architectural pattern achieves several crucial security objectives:

  • Reduced Attack Surface: Instead of monitoring and defending dozens of servers exposed to the internet, your security team only needs to harden and monitor one entry point.
  • Centralized Auditing: All administrative access flows through a single point, making it trivial to log who is accessing the infrastructure and when.
  • Network Isolation: If the bastion host is compromised, the attacker does not automatically gain network-level routing to the private subnet without further lateral movement techniques.

2. Legacy vs. Modern: ProxyCommand vs ProxyJump

For over a decade, SSH users relied on the ProxyCommand directive to tunnel connections through jump hosts. While functional, it was notoriously complex and difficult to read.

The Old Way: ProxyCommand

Using ProxyCommand required invoking a secondary SSH process with the -W flag (or using netcat) to forward standard input and output through the bastion.

# Legacy ~/.ssh/config approach
Host target-server
    HostName 10.0.5.42
    User deploy
    ProxyCommand ssh -q -W %h:%p [email protected]

This syntax tells the local SSH client to run the command ssh -q -W 10.0.5.42:22 [email protected] and then establish the target SSH connection over the resulting I/O stream. It worked, but chaining multiple hops was a nightmare of nested commands and quoting headaches.

The Modern Way: ProxyJump (-J)

Introduced in OpenSSH 7.3, ProxyJump (and the corresponding -J command-line flag) revolutionized how developers configure bastion hosts. It provides a native, built-in mechanism for routing traffic without invoking secondary commands.

# Modern ~/.ssh/config approach
Host target-server
    HostName 10.0.5.42
    User deploy
    ProxyJump [email protected]

This is undeniably cleaner, easier to read, and less prone to configuration errors. Beyond aesthetics, ProxyJump is also marginally faster as it handles the TCP forwarding natively within the SSH multiplexer, avoiding the overhead of piping through an external shell command. It is the definitive standard in 2026.

For a broader overview of SSH client configurations and keys, check out our SSH Complete Guide.

3. Deep Dive into ProxyJump Configuration

Let's break down the various ways you can utilize ProxyJump, starting from the command line and moving into persistent configurations.

Command Line Usage

When you need to quickly access a server without altering your configuration files, the -J flag is your best friend. The syntax is:

ssh -J [jump-user@]jump-host[:port] [target-user@]target-host

If your bastion host operates on a non-standard port (a common security practice to reduce automated scanner noise), you simply append the port to the jump host string:

ssh -J [email protected]:2222 [email protected]

Persistent Configuration in ~/.ssh/config

For daily operations, typing out the jump host arguments is inefficient. The ~/.ssh/config file allows you to define aliases and rules. A robust setup usually involves defining the bastion host independently, and then referencing it in the target host blocks.

# 1. Define the Bastion Host
Host bastion
    HostName bastion.example.com
    User security-admin
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_bastion

# 2. Define the Target Servers
Host db-primary
    HostName 10.0.5.100
    User postgres
    IdentityFile ~/.ssh/id_ed25519_internal
    ProxyJump bastion

Host web-node-*
    HostName %h.internal.example.com
    User deploy
    IdentityFile ~/.ssh/id_ed25519_internal
    ProxyJump bastion

In this example, typing ssh db-primary will automatically authenticate you against bastion using your bastion key on port 2222, establish the tunnel, and then authenticate you against 10.0.5.100 using your internal key. The local client handles everything seamlessly.

4. Multi-Hop SSH: Chaining Bastion Hosts

Enterprise environments often employ extreme network segmentation. You might find yourself needing to jump through a corporate VPN gateway, into a management DMZ, and finally into a highly secure production database subnet. This requires multi-hop SSH.

┌─────────┐      ┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│         │      │             │      │              │      │             │
│ Laptop  ├─────►│ Corp Jump 1 ├─────►│ Prod Jump 2  ├─────►│ DB Server   │
│         │      │ (Edge DMZ)  │      │ (Secure VPC) │      │ (Deep Sub)  │
│         │      │             │      │              │      │             │
└─────────┘      └─────────────┘      └──────────────┘      └─────────────┘

With ProxyJump, achieving this is trivial. You simply provide a comma-separated list of jump hosts. The connection routes through them sequentially.

Multi-Hop Command Line

ssh -J jump1.corp.com,jump2.prod.internal db.secure.internal

Multi-Hop Configuration

In your ~/.ssh/config, you can chain the references. Notice how we use the aliases we define as the jump targets.

Host jump1
    HostName jump1.corp.com
    User corp-user

Host jump2
    HostName 172.16.0.5
    User prod-admin
    ProxyJump jump1

Host secure-db
    HostName 10.100.0.50
    User dbadmin
    # Option A: Explicitly chain them
    ProxyJump jump1,jump2
    
    # Option B: Implicitly chain them (if jump2 already has a ProxyJump)
    # ProxyJump jump2

Note on Implicit Chaining: While OpenSSH does allow implicit chaining (where jumping to jump2 automatically triggers its own ProxyJump jump1 directive), explicitly listing the chain ProxyJump jump1,jump2 is widely considered a best practice for readability and debugging.

5. Best Practices for SSH Jump Host Security

A bastion host is a high-value target. If an attacker compromises it, they are one step closer to your private data. Hardening both the client config and the bastion server is critical.

1. Never Store Private Keys on the Bastion

The most common anti-pattern is copying your private SSH key to the jump host and running an SSH command from the bastion's shell. Never do this.

When you use ProxyJump, your local SSH client establishes an encrypted tunnel to the bastion, and then establishes a second encrypted tunnel through the first one to the target server. The cryptographic handshake for the target server happens entirely on your local machine. The bastion host only sees encrypted TCP traffic; it never sees your private key, and it never sees the plaintext data of your session.

2. Disable Agent Forwarding

Do not use ForwardAgent yes to authenticate through a jump host. If the jump host is compromised by an attacker with root privileges, they can hijack your forwarded agent socket and impersonate you across your infrastructure. ProxyJump eliminates the need for agent forwarding entirely.

3. Client-Side Generation of Complex Configs

As your ~/.ssh/config grows to manage hundreds of servers across multiple VPCs, generating and organizing the file becomes complex. However, using online tools that require you to upload your infrastructure topology is a massive security risk.

This is where ZeroData Tools excels. Because all our tools operate strictly in your browser via local JavaScript, your hostnames, IPs, and usernames never leave your machine. You get the convenience of a robust generator without the catastrophic risk of a third-party data breach exposing your network map.

4. Server-Side Hardening

On the jump host itself, ensure the /etc/ssh/sshd_config is locked down:

  • PermitRootLogin no
  • PasswordAuthentication no (Only allow Ed25519 keys)
  • AllowTcpForwarding yes (Required for ProxyJump to work)
  • X11Forwarding no
  • Use Fail2Ban or CrowdSec to immediately ban IP addresses that repeatedly fail authentication.

6. Advanced: Port Forwarding through a Jump Host

Sometimes you need more than an SSH shell. You might need to access a private internal dashboard, or connect a local database client (like DBeaver or DataGrip) to a private PostgreSQL instance.

You can seamlessly combine Port Forwarding (-L or -R) with ProxyJump.

# Forward local port 5432 to the target's port 5432, routing through the bastion
ssh -J [email protected] -L 5432:localhost:5432 -N -f [email protected]

Breaking this down:

  • -J [email protected] : Routes the connection through the jump host.
  • -L 5432:localhost:5432 : Binds local port 5432 to the remote target's localhost:5432.
  • -N : Do not execute a remote command (just forward ports).
  • -f : Send the SSH process to the background.

If you find port forwarding syntax confusing, use our visual SSH tunnel generator to construct these commands perfectly without uploading any data.

7. Common Firewall Pitfalls and Troubleshooting

Even with a perfect config, network realities can disrupt your connections. Here are the most common issues and how to resolve them.

Connection Drops / NAT Timeouts

If your SSH session drops after a few minutes of inactivity, it is likely a firewall or NAT router forcefully closing idle TCP connections. To fix this, tell your SSH client to send "keep-alive" packets.

# Add this to the top of your ~/.ssh/config
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

This sends a null packet every 60 seconds. If 3 packets fail consecutively, the client cleanly disconnects rather than hanging indefinitely.

DNS Resolution Failures

A massive advantage of ProxyJump is that DNS resolution for the target server is performed by the jump host, not your local machine. If you type ssh -J bastion target.internal, your local machine does not need to know the IP of target.internal.

However, if the bastion host's /etc/resolv.conf is misconfigured and it cannot resolve the internal DNS, the connection will fail with a "Could not resolve hostname" error. Always verify that the jump host can successfully ping or resolve the internal targets.

"Channel 0: open failed: administratively prohibited"

If you see this error, it means the connection reached the jump host, but the jump host's SSH daemon is actively blocking the port forwarding request required to reach the target. Check the bastion host's /etc/ssh/sshd_config and ensure that AllowTcpForwarding is set to yes.


Frequently Asked Questions

What is an SSH Jump Host (Bastion Host)?
An SSH Jump Host, or bastion host, is a hardened server exposed to the public internet that serves as a gateway to access other servers residing in a private network or private subnet. You SSH into the jump host first, and from there, SSH into the target server.
How does ProxyJump differ from the legacy ProxyCommand?
ProxyCommand is an older directive that required invoking a secondary command (like ssh -W or netcat) to pipe standard input and output through the bastion. ProxyJump (-J) was introduced in OpenSSH 7.3+ and is a built-in, native way to route traffic through one or multiple jump hosts. It is cleaner, faster, and less prone to quoting and escaping errors.
Can I chain multiple SSH jump hosts together (multi-hop SSH)?
Yes, you can chain multiple jump hosts by providing a comma-separated list to the ProxyJump directive. For example, ProxyJump user1@bastion1:22,user2@bastion2:2222. The connection will tunnel through bastion1, then bastion2, before reaching the final target.
Do I need to store my private SSH key on the jump host?
No. Never store your private keys on a jump host. Both ProxyJump and ProxyCommand forward the TCP connection directly to the target server over the encrypted tunnel, meaning the authentication handshake happens between your local machine and the final target. The jump host never sees your private key or the unencrypted traffic.
How do I specify a non-standard port for the jump host?
On the command line, use the syntax ssh -J user@jump-host:port target-server. In the ~/.ssh/config file, you create a Host block for the jump host and define its Port directive there, or use ProxyJump jump-host:port directly in the target block.
Is SSH Agent Forwarding safe to use with a bastion host?
Generally, no. Agent Forwarding (ForwardAgent yes) exposes your local SSH agent socket on the remote server. If the jump host is compromised and the attacker has root access, they can hijack your agent socket to authenticate as you. Always prefer ProxyJump, which does not require agent forwarding.
How does a ProxyJump handle DNS resolution for the target server?
With ProxyJump, DNS resolution of the target server's hostname happens entirely on the jump host, not on your local machine. This is ideal for private networks where the target server's internal DNS name (e.g., db-primary.internal.local) is unresolvable from the public internet.
Why is local client-side generation preferred for SSH configurations?
Uploading your server IPs, hostnames, network topologies, and usernames to a third-party server creates a severe security risk. Tools that process these configurations locally in your browser (like ZeroData Tools) ensure that your infrastructure layout remains entirely private and never leaves your device.