用 SSH ProxyJump 访问内网主机

内网主机通常没有公网地址,只允许从跳板机进入。最直接的做法是先登录跳板机,再从跳板机执行第二次 ssh;但这样会割裂本机的 SSH 配置,文件传输、端口转发和主机密钥管理也会变得麻烦。

ProxyJump 让本机 SSH 客户端通过跳板机建立到目标机的连接。私钥仍留在本机,最终主机仍由本机验证,日常命令也可以继续使用同一个主机别名。

最短答案

一次性连接只需要 -J

ssh -J ops@bastion.example.com deploy@app-01.internal

跳板机使用非默认端口时,把端口写在跳板机后面:

ssh -J ops@bastion.example.com:2222 deploy@app-01.internal

-JProxyJump 的命令行写法。参数格式为 [user@]host[:port];多个跳板机用逗号连接,并按顺序访问:

ssh -J ops@edge.example.com,ops@bastion.internal deploy@app-01.internal

这里的逗号两边不要加空格。IPv6 地址需要放进方括号。

ProxyJump 实际做了什么

根据 OpenSSH 的 ssh(1)ssh_config(5) 手册,连接分成两层:

  1. 本机先建立到跳板机的 SSH 连接。
  2. 跳板机为本机打开一条通向目标机 SSH 端口的 TCP 转发。
  3. 本机 SSH 客户端在这条转发上与目标机完成握手、主机密钥校验和用户认证。

它不是“先进入跳板机 Shell,再执行第二次 ssh”。因此:

项目由谁处理
跳板机认证本机 SSH 客户端
目标机认证本机 SSH 客户端
两台机器的主机密钥本机 known_hosts
到目标机的 TCP 连接跳板机发起
目标机的交互会话本机与目标机之间的 SSH 连接

跳板机能看到目标地址、连接时间和流量大小,但在正确验证目标机主机密钥的前提下,不能直接读取内层 SSH 会话的明文。

推荐的 SSH 配置

长期使用时,不要把用户名、端口和密钥路径塞进每条命令。先准备配置目录:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config

然后写入一份完整配置:

Host bastion
    HostName bastion.example.com
    User ops
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_bastion
    IdentitiesOnly yes
    ForwardAgent no

Host app-prod
    HostName app-01.internal
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ProxyJump bastion

之后只需要:

ssh app-prod

几个关键配置项:

配置作用
Host本机使用的别名
HostName实际主机名或 IP
IdentityFile该主机使用的本地私钥
IdentitiesOnly yes只尝试明确配置的身份,避免 agent 中密钥过多
ProxyJump bastion先通过 bastion 连接目标机
ForwardAgent no不把本机 agent 暴露给远端

目标机的命令行选项和配置不会自动套用到跳板机。跳板需要独立用户、端口或密钥时,应为它定义单独的 Host 配置;这是很多“明明指定了密钥,跳板机却仍然认证失败”的根源。

多台目标机与多级跳板

如果同一内网域名下的机器都经过同一个跳板机,可以使用 Host 通配规则:

Host bastion
    HostName bastion.example.com
    User ops
    IdentityFile ~/.ssh/id_ed25519_bastion
    IdentitiesOnly yes

Host *.internal
    User deploy
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ProxyJump bastion

现在可以直接连接:

ssh app-01.internal
ssh worker-02.internal

最终目标名由最后一台跳板机所在网络解析并连接,因此它可以是只在内网 DNS 中存在的名称。多级跳转时,每台中间跳板则由它的前一跳访问。

多级跳板也可以直接引用已经定义好的别名:

Host app-prod
    HostName app-01.internal
    User deploy
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ProxyJump edge,inner-bastion

连接路径变成 本机 → edge → inner-bastion → app-prodedgeinner-bastion 也应像前面的 bastion 一样,各自拥有完整的 Host 配置。

文件传输

配置好 Host app-prod 后,其他基于 SSH 的工具可以复用同一个别名。

上传文件:

scp ./release.tar.gz app-prod:/tmp/

打开 SFTP:

sftp app-prod

同步目录:

rsync -av --progress -e ssh ./dist/ app-prod:/srv/app/

端口映射与转发

ProxyJump 只改变本机如何到达 app-prod-L-R-D 仍建立在本机与最终目标机之间。判断方向时先看监听端口开在哪里:

模式监听端目标连接由谁发起常见用途
-L 本地转发本机最终 SSH 主机从本机访问内网数据库、管理后台
-R 远程转发最终 SSH 主机本机把本地开发服务临时提供给远端
-D 动态转发本机最终 SSH 主机建立 SOCKS4/5 代理

只建立隧道、不需要远程 Shell 时使用 -N。下面的监听地址都显式写成 127.0.0.1,避免端口意外暴露给局域网。

本地转发:把远端服务映射到本机

语法为:

-L [本地监听地址:]本地端口:目标地址:目标端口

把内网数据库映射到本机 15432 端口:

ssh -N -L 127.0.0.1:15432:db.internal:5432 app-prod

数据路径为:

本机 127.0.0.1:15432
  → SSH 隧道
  → app-prod
  → db.internal:5432

随后本机程序连接 127.0.0.1:15432db.internal 由最终 SSH 主机 app-prod 解析并连接,不是由本机或跳板机连接。

若服务只监听在 app-prod 自己的回环地址,可以这样映射:

ssh -N -L 127.0.0.1:18080:127.0.0.1:8080 app-prod

右侧的 127.0.0.1:8080app-prod 自己,而不是本机。需要同时映射多个端口时,可以重复传入 -L

远程转发:把本机服务映射到远端

语法为:

-R [远端监听地址:]远端端口:目标地址:目标端口

假设本机开发服务监听 127.0.0.1:3000,希望只让 app-prod 本机通过 19000 端口访问:

ssh -N -R 127.0.0.1:19000:127.0.0.1:3000 app-prod

数据方向与 -L 相反:

app-prod 127.0.0.1:19000
  → SSH 隧道
  → 本机 127.0.0.1:3000

这里右侧的目标由本机 SSH 客户端连接,所以 127.0.0.1:3000 指本机服务。

动态转发:建立 SOCKS 代理

-D 不固定目标地址,而是在本机启动一个 SOCKS4/5 服务;每个目标由使用代理的应用决定:

ssh -N -D 127.0.0.1:1080 app-prod

支持 SOCKS5 的应用连接 127.0.0.1:1080 后,请求会从 app-prod 所在网络发出。例如使用 curl 验证:

curl --proxy socks5h://127.0.0.1:1080 https://example.com/

对于 curl,socks5h 会把域名随 SOCKS 请求交给远端解析,而 socks5 会先在本机解析域名。

写入 SSH 配置

固定使用的映射适合单独定义一个别名,避免每次普通登录都占用本地端口:

Host app-prod-db
    HostName app-01.internal
    User deploy
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ProxyJump bastion

    LocalForward 127.0.0.1:15432 db.internal:5432
    SessionType none
    ExitOnForwardFailure yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

启动时只需要:

ssh app-prod-db

连接成功后会保持前台且通常没有输出;按 Ctrl-C 关闭隧道。

对应关系如下:

命令行~/.ssh/config
-LLocalForward
-RRemoteForward
-DDynamicForward
-NSessionType none

ExitOnForwardFailure yes 能在监听端口创建失败时立即退出,但不会因为最终服务拒绝连接或超时而退出。调试时建议保持前台运行;确认无误后若需要后台启动,可以使用:

ssh -fNT \
  -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:15432:db.internal:5432 \
  app-prod

-f 在认证后进入后台,-T 禁止分配伪终端。该命令不会自动重连,长期隧道应交给服务管理器监控。

密钥与主机身份边界

私钥只留在本机

ProxyJump 不要求把目标机私钥复制到跳板机,也不要求开启 agent forwarding。跳板机只承担 TCP 转发;目标机认证仍由本机 SSH 客户端完成。

不要为了“第二跳能用密钥”执行以下操作:

  • 把私钥复制到跳板机;
  • 全局设置 ForwardAgent yes
  • 为绕过首次连接直接设置 StrictHostKeyChecking no

OpenSSH 手册明确提醒:拿到远端 agent socket 访问权限的用户虽然不能导出私钥材料,却可以借用 agent 中的密钥执行签名认证。只有明确需要在远端继续发起 SSH 连接时,才应对单个可信主机临时启用 agent forwarding。

分别验证两台主机

首次连接会分别确认跳板机和目标机的主机密钥。应通过云控制台、运维平台或其他可信渠道核对指纹,再接受写入 ~/.ssh/known_hosts

服务端转发限制

ProxyJump 和业务端口转发是两层独立策略:跳板机控制到 app-prod:22 的代理连接,-L-R-D 则由 app-prod 的 sshd 决定是否放行。

客户端请求最终目标机的 sshd 策略
-L-DAllowTcpForwarding local;可用 PermitOpen 限制目标
-RAllowTcpForwarding remote;可用 PermitListen 限制监听地址和端口
-R 0.0.0.0:...还需要 GatewayPorts clientspecifiedyes

ProxyJump 依赖每一台跳板机的 SSH 服务允许 TCP forwarding。若你负责维护跳板机,可以把该账户允许转发的目标限制到指定主机:

Match User jump
    AllowTcpForwarding local
    PermitOpen app-01.internal:22

这段属于跳板机的 /etc/ssh/sshd_config,不是本机的 ~/.ssh/config。它只限制 TCP 转发目标,并不自动禁用该账户的 Shell;完整的专用跳板账户还需要单独设计登录权限。

DisableForwarding yes 会覆盖其他转发配置并全部禁用。若账户仍拥有正常 Shell,仅禁用 SSH forwarding 并不能阻止用户自行运行其他转发程序。具体限制语义见 OpenSSH sshd_config(5) 手册。

如果服务端策略看起来正确但仍被拒绝,还要检查 authorized_keys 中的 restrictno-port-forwardingpermitopenpermitlisten 等单个密钥限制。

ProxyJump 与 ProxyCommand

早期配置经常使用:

Host app-prod
    HostName app-01.internal
    ProxyCommand ssh -W %h:%p bastion

对普通 SSH 跳板机,ProxyJump bastion 更直接,也原生支持逗号分隔的多级跳转。ProxyCommand 仍适合 HTTP/SOCKS 代理或需要自定义连接程序的场景。

不要在同一个目标配置中同时设置 ProxyJumpProxyCommand。两者会竞争同一条代理连接,OpenSSH 使用先取得的值,后面的配置不会覆盖它。

排查顺序

1. 查看最终生效配置

ssh -G 会展开 HostMatch 后输出最终配置:

ssh -G app-prod |
  grep -E '^(hostname|user|port|identityfile|proxyjump) '

ssh -G bastion |
  grep -E '^(hostname|user|port|identityfile|identitiesonly) '

第一条命令应显示目标地址、目标用户、目标密钥和 proxyjump bastion;第二条用于确认第一跳实际使用的用户、端口与密钥。

2. 分别验证每一段

先确认本机能完成跳板认证,并建立到目标 SSH 端口的 TCP forwarding:

ssh -vvv -W app-01.internal:22 bastion

成功后终端通常会出现目标 SSH banner;按 Ctrl-C 结束。若跳板账户允许交互 Shell,也可以直接运行 ssh bastion,但专用跳板账户可能只允许转发。

再从跳板机检查目标端口;以下命令要求跳板机安装了 nc

ssh bastion 'nc -vz app-01.internal 22'

最后开启详细日志:

ssh -vvv app-prod

日志应依次出现跳板机认证、stdio forwarding 建立,以及目标机认证。

3. 单独验证端口转发

先以前台模式建立映射并保留详细日志:

ssh -vvv -N \
  -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:15432:db.internal:5432 \
  app-prod

另开终端确认本机正在监听。Ubuntu 使用:

ss -lntp | grep ':15432'

macOS 使用:

lsof -nP -iTCP:15432 -sTCP:LISTEN

监听成功只说明隧道入口已经建立。先从本机发起一次连接,验证完整的 -L 路径:

nc -vz 127.0.0.1 15432

如果失败,再从目标连接的发起端检查业务地址,以区分隧道问题和 app-prod → db.internal 的连通问题:

ssh app-prod 'nc -vz db.internal 5432'

对于 -R,保持隧道运行后从远端验证监听端口:

ssh app-prod 'nc -vz 127.0.0.1 19000'

对于 -D,使用前文的 socks5h:// curl 命令验证代理和远端 DNS。

4. 对照常见错误

现象优先检查
跳板机认证失败Host bastionUserPortIdentityFile
Too many authentication failures两个 Host 是否都设置了 IdentitiesOnly yes
administratively prohibitedssh -vvv 确认是哪台 sshd 拒绝,再检查 sshd 与 authorized_keys 的转发限制
建立 ProxyJump 时目标 HostName 无法解析最后一台跳板机能否解析目标机地址
Address already in use转发监听端口是否已被占用
remote port forwarding failed远端端口占用,或 AllowTcpForwardingPermitListenGatewayPorts 拒绝
connect failed: Name or service not known-L-D 检查最终 SSH 主机的解析;-R 检查本机解析
SOCKS 可连接但内网域名失败应用可能在本机解析 DNS;curl 改用 socks5h://
目标机主机密钥冲突实际目标是否改变;不要直接删除记录后盲目接受
设置了 ProxyJump 却走 ProxyCommand配置文件中谁先取得代理配置

若通配规则默认启用了跳板,但某台主机可以直连,可单次禁用:

ssh -o ProxyJump=none public-host

验证通过后,日常只需要记住 app-prod 这个别名。跳板地址、端口、用户和两套密钥由 SSH 配置统一管理,sshscpsftprsync 与端口转发共享同一条连接路径。

参考资料

这篇怎么样?

评论区将在滚动到这里时加载。