Multi-Hop SSH (2-Hop & 3-Hop) — A Practical Guide for Your Homelab

Make SSH jumps painless with ProxyJump, clean ~/.ssh/config, and copy-paste examples that “just work.”


Why multi-hop?

When your target machine isn’t directly reachable from the internet (e.g., sits behind a VPS jump host or a Raspberry Pi in your LAN), you can chain SSH connections through one or more “jump hosts.” This keeps your perimeter tight and avoids exposing extra ports.


Quick Start: One-off Commands

2 hops (Laptop → Jump A → Target B)

ssh -J userA@jumpA:portA userB@targetB
# Example:
# ssh -J root@157.90.125.67 pi@10.0.0.7

3 hops (Laptop → Jump A → Jump B → Target C)

ssh -J userA@jumpA,userB@jumpB userC@targetC
# Example:
# ssh -J root@157.90.125.67,pi@10.0.0.7 bill@192.168.0.50
Tip: Use user@host:port if a hop isn’t on port 22.

The Durable Way: ~/.ssh/config

Create (or edit) ~/.ssh/config (Windows: C:\Users\<you>\.ssh\config). This makes commands short, readable, and reusable.

Example topology

Laptop → VPS (157.90.125.67) → Raspberry Pi (10.0.0.7) → Desktop (192.168.0.50)
# 1) First jump: VPS
Host vps
  HostName 157.90.125.67
  User root
  Port 22
  IdentityFile ~/.ssh/id_ed25519

# 2) Second hop: Home Raspberry Pi (reachable from VPS)
Host homepi
  HostName 10.0.0.7
  User pi
  Port 22
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump vps

# 3) Final target: Home Desktop (reachable from Raspberry Pi)
Host homedesktop
  HostName 192.168.0.50
  User bill
  Port 22
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump homepi

# Quality-of-life defaults
Host *
  ServerAliveInterval 30
  ServerAliveCountMax 3
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m
  StrictHostKeyChecking accept-new

Now connect with:

ssh homedesktop

Compact 3-hop variant (all jumps inline)

Host homedesktop
  HostName 192.168.0.50
  User bill
  ProxyJump root@157.90.125.67,pi@10.0.0.7

Set Up the Keys (do this once)

  1. Install keys hop-by-hop:
  2. Connectivity check: ensure each hop can reach the next (private IPs/WireGuard subnets/hostnames resolvable from that hop).

From the Pi, copy the Pi public key to the final target (Desktop)

ssh-copy-id bill@192.168.0.50

From the VPS, copy the VPS public key to the second hop (Raspberry Pi)

ssh-copy-id pi@10.0.0.7

Copy your laptop’s public key to the first jump (VPS)

ssh-copy-id root@157.90.125.67

Generate a key on your laptop (skip if you already have one):

ssh-keygen -t ed25519 -C "laptop"
Alternative: use ~/.ssh/authorized_keys manually if ssh-copy-id isn’t available.

File Transfer over Multiple Hops

scp

# With inline ProxyJump
scp -o ProxyJump=root@157.90.125.67,pi@10.0.0.7 ./local.dat bill@192.168.0.50:/tmp/

# If you configured Host aliases
scp ./local.dat homedesktop:/tmp/

rsync

rsync -avz -e "ssh -J root@157.90.125.67,pi@10.0.0.7" ./localdir/ bill@192.168.0.50:/data/localdir/

When ProxyJump Isn’t Available: ProxyCommand (Fallback)

On very old OpenSSH clients/servers:

Host homepi
  HostName 10.0.0.7
  User pi
  ProxyCommand ssh -W %h:%p root@157.90.125.67

Host homedesktop
  HostName 192.168.0.50
  User bill
  ProxyCommand ssh -J root@157.90.125.67 -W %h:%p pi@10.0.0.7
If -W isn’t supported on the jump host, replace with ProxyCommand ssh root@157.90.125.67 nc %h %p (requires nc).

Security & Convenience Tips

  • Agent forwarding — only for trusted jumps:
    • Temporary: ssh -A homedesktop
    • Persistent (per host in config): ForwardAgent yes
  • Connection reuse — the ControlMaster/ControlPersist settings make repeated commands much faster.
  • Keep-alivesServerAliveInterval 30 helps prevent idle disconnects.
  • Host key hygiene — first connect will add entries to ~/.ssh/known_hosts. If a server is legitimately rebuilt or rekeyed and you see
    REMOTE HOST IDENTIFICATION HAS CHANGED!
    verify the change, then edit the corresponding line in known_hosts.

Windows Notes (OpenSSH & PuTTY)

  • Windows 10/11 (PowerShell) ships with OpenSSH; everything above works the same as on Linux/macOS. Config file path:
    C:\Users\<you>\.ssh\config
  • PuTTY alternative: set Connection → SSH → Tunnels/Proxy with
    “Local proxy command” = plink -ssh -W %host:%port user@jump.
    (OpenSSH config is simpler; prefer that if possible.)

Troubleshooting

  • “Permission denied (publickey)”
    • Confirm the correct IdentityFile per host.
    • Check file permissions on the server: chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys.
  • Can’t reach next hop
    • From each hop, ping/ssh the next machine by its next-hop-visible address (private IP/WG IP).
    • Check Firewalls / sshd_config (AllowUsers, ListenAddress, etc.).
  • Frequent disconnects
    • Add keep-alives in config (see above).
    • Network/NAT timeouts can be mitigated with a smaller ServerAliveInterval (e.g., 15).

Debug verbose

ssh -vvv homedesktop

Watch where the chain fails.


Cheat Sheet

  • 2 hops
    ssh -J userA@jumpA userB@targetB
  • 3 hops
    ssh -J userA@jumpA,userB@jumpB userC@targetC
  • Config alias
    ssh homedesktop
  • SCP with jumps
    scp -o ProxyJump=userA@jumpA,userB@jumpB file userC@target:/path/
  • Rsync with jumps
    rsync -avz -e "ssh -J userA@jumpA,userB@jumpB" src/ userC@target:/dst/
  • Fallback
    ProxyCommand ssh -W %h:%p user@jump

Minimal Checklist (Do Once)

  • Laptop SSH key created (ed25519 recommended).
  • Public keys installed hop-by-hop to authorized_keys.
  • Each hop can resolve and reach the next hop.
  • ~/.ssh/config written with ProxyJump and quality-of-life settings.
  • Tested ssh homedesktop, scp, rsync, and ssh -vvv for debugging.