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)
- Install keys hop-by-hop:
- 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_keysmanually ifssh-copy-idisn’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-Wisn’t supported on the jump host, replace withProxyCommand ssh root@157.90.125.67 nc %h %p(requiresnc).
Security & Convenience Tips
- Agent forwarding — only for trusted jumps:
- Temporary:
ssh -A homedesktop - Persistent (per host in config):
ForwardAgent yes
- Temporary:
- Connection reuse — the
ControlMaster/ControlPersistsettings make repeated commands much faster. - Keep-alives —
ServerAliveInterval 30helps 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 seeREMOTE HOST IDENTIFICATION HAS CHANGED!
verify the change, then edit the corresponding line inknown_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
IdentityFileper host. - Check file permissions on the server:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys.
- Confirm the correct
- Can’t reach next hop
- From each hop,
ping/sshthe next machine by its next-hop-visible address (private IP/WG IP). - Check Firewalls /
sshd_config(AllowUsers,ListenAddress, etc.).
- From each hop,
- 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 (
ed25519recommended). - Public keys installed hop-by-hop to
authorized_keys. - Each hop can resolve and reach the next hop.
~/.ssh/configwritten withProxyJumpand quality-of-life settings.- Tested
ssh homedesktop,scp,rsync, andssh -vvvfor debugging.