Triple‑Hop SSH, Clean & Secure: A Blog‑Ready Tutorial (All Sensitive Info Redacted)

Author: Jiahong — This guide hides all real IPs, domains, usernames, and keys. Replace placeholders as needed.


TL;DR

  • Use ProxyJump (-J) for a clean multi‑hop SSH: local → Jump #1 (VPS) → Jump #2 (Edge/RPi) → Target.
  • Store everything in ~/.ssh/config with separate keys per host.
  • Secure defaults: no password logins, no agent forwarding (unless required), keepalives on.
  • Transfer files with scp -J or rsync -e 'ssh -J ...' the same way.
  • Works with port forwarding, too.

Network Topology (Example / Redacted)

We’ll use documentation‑safe example addresses and domains:

  • Local machine: your laptop (macOS/Linux/Windows + OpenSSH)
  • Jump #1 (VPS): vps.example.net (public Internet)
  • Jump #2 (Edge / RPi): 198.51.100.7 (e.g., VPN private range)
  • Target (Desktop): 192.168.50.10 (home LAN)
NotesDomains like example.net and IP blocks like 198.51.100.0/24, 203.0.113.0/24, and 192.0.2.0/24 are reserved for documentation per RFC 5737. Safe to show in screenshots.
flowchart LR
    A[Local] -- SSH --> B[Jump #1\nVPS\nvps.example.net]
    B -- SSH --> C[Jump #2\nEdge/RPi\n198.51.100.7]
    C -- SSH --> D[Target Desktop\n192.168.50.10]

1) Generate and Install SSH Keys (Per‑Host Keys)

Use distinct keypairs for each hop for least privilege.

# On LOCAL
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_local -C "local@host"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_vps -C "vps@login"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rpi -C "rpi@login"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_target -C "target@login"

# Copy public keys to each host (replace placeholders)
ssh-copy-id -i ~/.ssh/id_ed25519_vps.pub user_vps@vps.example.net
# From VPS to RPi (or via ProxyJump once configured)
ssh-copy-id -i ~/.ssh/id_ed25519_rpi.pub user_rpi@198.51.100.7
# From RPi to Target
ssh-copy-id -i ~/.ssh/id_ed25519_target.pub user_target@192.168.50.10

Server‑side hardening (on VPS, RPi, Target — /etc/ssh/sshd_config):

PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
AllowTcpForwarding yes
GatewayPorts no
PermitTunnel no
ClientAliveInterval 30
ClientAliveCountMax 3

Reload SSH:

sudo systemctl reload sshd

2) Modern, Minimal: ProxyJump (OpenSSH ≥ 7.3)

Create or edit LOCAL ~/.ssh/config:

# === Jump #1: VPS ===
Host vps
  HostName vps.example.net         # PUBLIC
  User user_vps                    # change me
  Port 22
  IdentityFile ~/.ssh/id_ed25519_vps
  ServerAliveInterval 30
  ServerAliveCountMax 3

# === Jump #2: Edge / RPi ===
Host rpi
  HostName 198.51.100.7            # PRIVATE (VPN/edge)
  User user_rpi                    # change me
  Port 22
  IdentityFile ~/.ssh/id_ed25519_rpi
  ProxyJump vps                    # hop via VPS
  ServerAliveInterval 30
  ServerAliveCountMax 3

# === Target Desktop ===
Host desktop
  HostName 192.168.50.10           # HOME LAN
  User user_target                 # change me
  Port 22
  IdentityFile ~/.ssh/id_ed25519_target
  ProxyJump vps,rpi                # core: vps → rpi → desktop
  ServerAliveInterval 30
  ServerAliveCountMax 3

# === Quality of life ===
Host *
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m
  HashKnownHosts yes
  StrictHostKeyChecking accept-new

Connect with one command:

ssh desktop

3) File Copy & Sync (Multi‑Hop the Same Way)

SCP via multi‑hop:

scp -J vps,rpi ./local.file desktop:~/dest/

rsync via multi‑hop:

rsync -avP -e 'ssh -J vps,rpi' ./data/ desktop:/data/
Tip: After the first connection, ControlMaster multiplexing makes subsequent transfers faster.

4) Port Forwarding (Tunnels Across All Hops)

Example: expose Target’s Jupyter :8888 to LOCAL.

ssh -J vps,rpi -L 8888:127.0.0.1:8888 desktop
# then open http://127.0.0.1:8888 on LOCAL

Reverse forward (Target service → VPS public port), if you must:

# On TARGET (or from LOCAL with -J):
ssh -J vps,rpi -R 0.0.0.0:18080:127.0.0.1:8080 vps
# Now service on Target:8080 is reachable at VPS:18080 (firewall permitting)
Security note: Reverse forwards can expose internal services. Restrict with firewalls and only bind to 127.0.0.1 unless you truly need external access.

5) Old Systems: ProxyCommand -W (OpenSSH < 7.3)

Fallback if -J is unavailable:

Host vps
  HostName vps.example.net
  User user_vps
  IdentityFile ~/.ssh/id_ed25519_vps

Host rpi
  HostName 198.51.100.7
  User user_rpi
  IdentityFile ~/.ssh/id_ed25519_rpi
  ProxyCommand ssh -W %h:%p vps

Host desktop
  HostName 192.168.50.10
  User user_target
  IdentityFile ~/.ssh/id_ed25519_target
  ProxyCommand ssh -W %h:%p rpi

Connect:

ssh desktop

6) Security Checklist (Copy‑Paste Ready)

  • Unique ed25519 key per host; keep private keys only on LOCAL.
  • PasswordAuthentication no on all servers.
  • Firewall on VPS (e.g., ufw + Fail2ban); change SSH port if desired.
  • No ForwardAgent by default. If you must, limit to trusted jump hosts.
  • Keepalives: ServerAliveInterval/CountMax (client) and ClientAliveInterval/CountMax (server).
  • StrictHostKeyChecking accept-new and monitor for host key changes.
  • Regularly rotate keys; remove unused authorized_keys.

7) Troubleshooting (From Simple to Deep)

  1. Test hop by hop
ssh vps
ssh -J vps rpi
ssh -J vps,rpi desktop
  1. RPi → Target fails: verify LAN reachability, target firewall (port 22), and actual IP.
  2. Idle drops: increase keepalives; check middleboxes/NAT timeouts.
  3. Host key mismatch: host rebuilt? Remove old key: ssh-keygen -R <host> then reconnect.
  4. -J unknown: use ProxyCommand -W fallback.
  5. Agent forwarding errors: avoid ForwardAgent; copy public keys instead.

8) Redaction Policy for Blog/Screenshots

  • Use example.net, example.com, example.org for domains.
  • Use RFC‑5737 IPs: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24.
  • Obfuscate usernames (user_vps, user_rpi, user_target).
  • Blur fingerprints and paths in screenshots if present.

9) Copy‑Ready Template (Paste, Then Replace Placeholders)

# (1) VPS
Host vps
  HostName vps.example.net
  User user_vps
  IdentityFile ~/.ssh/id_ed25519_vps

# (2) Edge / RPi
Host rpi
  HostName 198.51.100.7
  User user_rpi
  IdentityFile ~/.ssh/id_ed25519_rpi
  ProxyJump vps

# (3) Target
Host desktop
  HostName 192.168.50.10
  User user_target
  IdentityFile ~/.ssh/id_ed25519_target
  ProxyJump vps,rpi

10) FAQ

Q: Can I add a fourth hop?
A: Yes: ProxyJump a,b,c,d scales, but complexity and latency increase. Prefer consolidating.

Q: How do I share one config across machines?
A: Store in a private dotfiles repo but keep keys out of VCS. Use a password manager or hardware tokens.

Q: Windows?
A: Use Windows 10/11 OpenSSH (built‑in) or Git Bash. ~/.ssh/config works the same.


中文版:三级跳 SSH(本地 → 跳板1 → 跳板2 → 目标)终极教程(已脱敏)

说明:本文示例中的域名与 IP 全部为文档保留地址(RFC 5737),用户名也为占位名。请在实际部署时替换为你的真实信息。

拓扑与思路

  • 本地电脑(LOCAL)
  • 跳板1(VPS,公网):vps.example.net
  • 跳板2(边缘/树莓派,私网/VPN):198.51.100.7
  • 目标主机(家庭局域网):192.168.50.10
flowchart LR
    L[本地] -- SSH --> J1[跳板1\nVPS\nvps.example.net]
    J1 -- SSH --> J2[跳板2\n树莓派\n198.51.100.7]
    J2 -- SSH --> T[目标主机\n192.168.50.10]

1)密钥与最小权限

为每台机器生成独立密钥,并仅在对应机器放入公钥。

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_vps -C "vps@login"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rpi -C "rpi@login"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_target -C "target@login"
ssh-copy-id -i ~/.ssh/id_ed25519_vps.pub user_vps@vps.example.net
ssh-copy-id -i ~/.ssh/id_ed25519_rpi.pub user_rpi@198.51.100.7
ssh-copy-id -i ~/.ssh/id_ed25519_target.pub user_target@192.168.50.10

服务器侧 sshd_config 关键项:

PasswordAuthentication no
PubkeyAuthentication yes
AllowTcpForwarding yes
ClientAliveInterval 30
ClientAliveCountMax 3

2)推荐写法:ProxyJump(≥ 7.3)

本地 ~/.ssh/config

Host vps
  HostName vps.example.net
  User user_vps
  IdentityFile ~/.ssh/id_ed25519_vps

Host rpi
  HostName 198.51.100.7
  User user_rpi
  IdentityFile ~/.ssh/id_ed25519_rpi
  ProxyJump vps

Host desktop
  HostName 192.168.50.10
  User user_target
  IdentityFile ~/.ssh/id_ed25519_target
  ProxyJump vps,rpi

Host *
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m
  StrictHostKeyChecking accept-new

连接:

ssh desktop

3)文件传输与同步

scp -J vps,rpi ./local.file desktop:~/dest/
rsync -avP -e 'ssh -J vps,rpi' ./data/ desktop:/data/

4)端口转发

ssh -J vps,rpi -L 8888:127.0.0.1:8888 desktop
# 本机浏览器访问 http://127.0.0.1:8888

5)旧版兼容:ProxyCommand -W

Host desktop
  HostName 192.168.50.10
  User user_target
  IdentityFile ~/.ssh/id_ed25519_target
  ProxyCommand ssh -W %h:%p rpi

6)安全清单

  • 禁用密码登录,仅密钥。
  • 不默认开启 ForwardAgent
  • 跳板只开放必要端口,结合防火墙 + Fail2ban。
  • 开启 keepalive,避免闲置断线。
  • 定期轮换密钥,清理 authorized_keys

7)常见故障

  • 分跳测试:ssh vpsssh -J vps rpissh -J vps,rpi desktop
  • RPi 到目标不通:检查二层/路由、防火墙、IP 是否正确。
  • -J 不可用:改用 ProxyCommand -W

8)脱敏规范

  • 域名用 example.net/.com/.org
  • IP 用 192.0.2.0/24198.51.100.0/24203.0.113.0/24
  • 用户名用占位符,指纹/路径打码。

You’re done. One command to rule the route:

ssh desktop