本地开发机连公司内网服务器,最大的痛点不是 SSH 本身,而是“中间还有一层跳板机”。生产环境、测试环境通常都藏在某个内网段里,本地电脑直连不上,必须先连一台有公网入口、同时能访问内网的那台机器,再从那台机器跳到目标服务器。以前这套流程只能在终端里敲命令凑合,现在 VSCode Remote SSH 可以把这条链路完整搬进编辑器,连远程服务器、甚至远程服务器里的 Docker 容器,都能像写本地代码一样开发和调试。这篇就是我实际配置 VSCode Remote SSH 借助跳板机连接远程服务器及远端容器的完整记录,适合所有被内网服务器折腾过的后端、运维,以及需要在容器里做开发的同学参考。
1. 先理清楚:为什么本地机连不上一台“需要跳板机”的服务器
1.1 跳板机场景的现状
很多人第一次遇到跳板机时会有个疑问:远程服务器不是有 IP 吗,凭什么本地连不上?原因很简单,目标服务器通常只有内网 IP,比如10.10.x.x,而公司网络的边界上只有跳板机有公网 IP 或能和本地电脑建立可达连接。安全策略要求所有对内网服务器的访问都必须经过跳板机,这叫“统一入口、统一审计”。所以真实网络拓扑往往是这样:
- 本地开发机可以访问跳板机
- 跳板机可以访问目标服务器所在的内网网段
- 本地开发机不能直接访问目标服务器
这种场景下,你要从本地登录目标服务器,必须形成“本地 -> 跳板机 -> 目标机”的两级链路。终端里可以连续执行两次ssh,但 VSCode Remote SSH 需要的是一个可直接连接的“最终主机”,这就要求我们把这两段链路在 SSH 层面“拼接”起来。
1.2 SSH 的“两级跳”到底发生了什么
先拆解一下 SSH 的跳转过程。你执行ssh user@target时,SSH 客户端会向 target 的 22 端口发起连接,然后协商认证。如果 target 不在本地网络里,连接就失败了。加了一台跳板机后,实际上你希望的是:本地先连上跳板机,然后由跳板机替你向 target 发起连接,并把两端的数据流进行中转。
这个“替你连”的动作,就是 OpenSSH 的-J参数和ProxyJump配置在做的事。它相当于客户端告诉跳板机:帮我建立一个到 target 的通道,然后我直接通过这条通道完成 SSH 认证和会话。对目标服务器来说,连接的来源是跳板机,所以只要跳板机有权限进入内网,链路就走得通。
1.3 为什么 VSCode Remote SSH 也能走通这条链路
VSCode Remote SSH 本质上是本地 VSCode 通过 SSH 在远程服务器上安装并启动一个vscode-server服务,然后本地窗口和远程服务之间建立通信。它并不关心你的 SSH 连接有几个中间节点,只要本地能通过 SSH 到达最终主机,它就能正常工作。所以你完全可以在~/.ssh/config里把“经过跳板机跳转”这一层配置好,让 VSCode 直接选择目标机的别名发起连接,剩下的跳转逻辑由 SSH 客户端自行处理。
2. SSH 跳板机制原理与两种配置写法
2.1 ProxyJump:最省事的写法
OpenSSH 从 7.3 开始支持ProxyJump,在ssh_config里的写法非常简单:
# ~/.ssh/config Host jump HostName 1.2.3.4 User ops Port 22 IdentityFile ~/.ssh/id_ed25519 Host prod-web HostName 10.10.10.10 User root ProxyJump jump IdentityFile ~/.ssh/id_ed25519这里Host prod-web是目标机的别名,HostName是内网 IP,ProxyJump jump声明了要经过的跳板机。配置好之后,终端里执行ssh prod-web,或者 VSCode Remote SSH 里选择prod-web,会自动先连 jump,再连 10.10.10.10,整个过程和直连一样顺滑。
这种写法的好处是逻辑清晰,语义直白,而且支持多级跳转,比如ProxyJump jump1,jump2,会依次经过这两台机器。多数现代 Linux 发行版和 macOS 自带的 OpenSSH 都支持这个参数,Windows 上只要用的是新版 OpenSSH 也没有问题。
2.2 ProxyCommand:老写法但仍有存在价值
如果你的跳板机或者本地 OpenSSH 版本比较老,不支持ProxyJump,可以退而求其次使用ProxyCommand:
Host prod-web HostName 10.10.10.10 User root ProxyCommand ssh -W %h:%p jump-W表示让跳板机进入“转发模式”,把标准输入输出和最终主机的 22 端口直接打通。%h和%p是 SSH 配置里的内置变量,会替换成目标机的 HostName 和端口。这种写法是ProxyJump的底层实现方式,如果哪天ProxyJump因为 OpenSSH 版本问题不可用,可以把配置临时改成这种形式。
另外还有一种比较老的写法叫nc代理转发,例如ProxyCommand ssh jump nc %h %p。这种方式依赖跳板机上装了netcat,而且每次都会多启动一个进程,实际用起来远没有-W干净,我不建议新项目再踩这个坑。
2.3 密钥、Agent 转发和“二级密钥”的处理思路
使用跳板机时,密钥管理容易让人绕晕。这里有一个关键点:目标机的认证发生在第二条链路上,本地 SSH 客户端会用配置里针对目标机指定的IdentityFile做认证,而这把私钥的验证过程是通过跳板机转发过去的。
如果你在跳板机上还保存了一份专门用于目标机的私钥,也可以实现在跳板机上手动登录目标机。但从 VSCode Remote SSH 的角度,我更推荐把目标机的私钥放在本地,然后用ssh-agent配合配置完成认证。具体做法是:
- 将相关私钥加入本地,比如执行
ssh-add ~/.ssh/id_ed25519。 - 在
~/.ssh/config中开启 Agent 转发:ForwardAgent yes。 - 跳板机上如果有限制,需要允许转发,一般是跳板机 SSH 服务端配置
/etc/ssh/sshd_config里的AllowAgentForwarding yes。
注意:如果跳板机上的 OpenSSH 服务端把
AllowTcpForwarding或AllowAgentForwarding关掉了,ProxyJump依然可能工作(因为它建立的是 TCP 转发通道),但 agent 转发可能会失败,这点排查时要区分清楚。
3. 实操:本地机 -> 跳板机 -> 远程服务器的完整配置
3.1 准备阶段:确认三端 OpenSSH 版本与网络可达性
在动手改配置之前,先确认三件事。第一,本地机器上的 OpenSSH 客户端版本,执行ssh -V,能看到版本号即可,只要能显示版本基本上都支持ProxyJump。第二,确认本地能访问跳板机,执行ssh ops@跳板机IP,能顺利登录再继续。第三,确认跳板机到目标服务器的网络连通性,可以登录跳板机后执行:
ping 10.10.10.10或
ssh root@10.10.10.10如果跳板机到目标机也畅通,那么链路可行,剩下的事情就是写配置。
3.2 编辑~/.ssh/config:一套配置同时解决 VSCode 与终端
我实际工作中的习惯是给每个环境都建一个别名,比如测试环境叫test-api,生产环境叫prod-web,这样后续在 VSCode 里选主机时非常直观。完整的配置示例:
# 跳板机 Host bastion HostName 192.168.30.5 User ops Port 22 IdentityFile ~/.ssh/id_ed25519 ForwardAgent yes ServerAliveInterval 30 ServerAliveCountMax 3 # 目标服务器 1 Host prod-web HostName 10.10.10.10 User root ProxyJump bastion IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30 # 目标服务器 2 Host prod-db HostName 10.10.10.11 User deploy ProxyJump bastion IdentityFile ~/.ssh/id_ed25519ServerAliveInterval 30的作用是每 30 秒发送一个保活包,防止长时间没有操作时连接被网络设备强制断开。这个参数在跳板机场景下尤其重要,因为链路多了一段,任何一个中间节点把空闲连接回收,你都会突然发现 VSCode 卡住不动。另外如果觉得每次连接都要确认主机指纹很烦,可以在全局配置里取消严格校验:
Host * StrictHostKeyChecking no UserKnownHostsFile /dev/null不过这个全局配置只建议用于内网测试环境,生产环境最好保留StrictHostKeyChecking ask,避免中间人风险。
配置写完后,先用终端验证一下:
ssh prod-web如果顺利进入目标机,说明链路配置正确。VSCode 里再安装 Remote-SSH 扩展,命令面板里执行Remote-SSH: Connect to Host,选择prod-web即可。
3.3 落地验证:从 VSCode 发起连接并排障
第一次在 VSCode 里连接时,它会在远程主机上自动下载对应版本的vscode-server,这个过程受网络环境影响较大。如果远程服务器无法访问外网,就会卡在“Setting up SSH Host”界面。遇到这个问题,有几个处理顺序:
- 先确认终端的
ssh prod-web能不能正常登录。 - 再确认远程主机能否访问更新服务器,手动执行
curl -I https://update.code.visualstudio.com测试。如果远程主机不能访问外网,但本地可以,可以给 VSCode 设置remote.SSH.allowLocalServerDownload为 true,让本地从官网下载后通过 SFTP 推送到远程。 - 如果远程主机能访问外网,但仍然卡住,多半是网络用户代理问题,需要在 VSCode 设置里配置
http.proxy和https.proxy。
注意:vscode-server 版本和本地 VSCode 版本强相关,如果本地自动更新过 VSCode,而远程主机上的 vscode-server 没有随之更新,VSCode 通常会重新下载安装。这个过程偶尔会留下旧版本目录,遇到异常时可以
rm -rf ~/.vscode-server后重连。
一旦 VSCode 进入远程窗口,左下角会显示“SSH: prod-web”,此时你可以直接打开远程服务器上的文件夹,插件也会以远程模式运行。终端面板会自动打开到目标机的 shell,调试、代码跳转、端口转发都与本地开发几乎无差别。
4. 进阶:把 VSCode 连到远程服务器里的 Docker 容器
4.1 方案A:容器自带 SSH,通过宿主端口映射再连容器
如果你想在目标服务器的 Docker 容器里开发,最简单的方案是让容器运行一个 SSHD 服务,并把宿主机的某个端口映射到容器的 22 端口。启动容器时可以这样:
docker run -d --name dev-container -p 2222:22 my-dev-image以前面的跳板配置为基础,你可以在~/.ssh/config中新增一个主机条目,把最终 HostName 指向宿主机,端口指向映射端口:
Host prod-container HostName 10.10.10.10 User dev Port 2222 ProxyJump bastion IdentityFile ~/.ssh/id_ed25519_dev这样 VSCode 选择prod-container时,会先经过跳板机,再连到宿主机的 2222 端口,最终进入容器里的 SSHD。这个方案思路简单,稳定性也不错,但要求容器镜像里必须安装并启动openssh-server,同时设置好密码或密钥认证。为了让 Key 认证可用,构建镜像时要把公钥写入容器的~/.ssh/authorized_keys文件。
4.2 方案B:SSH 端口转发把远端容器端口映射到本地
如果不想在容器里装 SSHD,另一个思路是做端口转发,把远程宿主机上已经映射出来的端口(比如 2222)再映射到本地某个端口,然后 VSCode 直接连localhost。这样跳板链路在外层完成,VSCode 的配置和普通直连没有区别。
操作时先执行:
ssh -N -L 2222:10.10.10.10:2222 bastion这个命令的含义是:通过跳板机bastion建立一条隧道,把本地 2222 端口转发到10.10.10.10的 2222 端口。-N表示不执行远程命令,只做转发。隧道建立后,在 VSCode 里连接:
Host local-container HostName 127.0.0.1 User dev Port 2222这个方案的好处是不需要每次都在~/.ssh/config里写复杂的 ProxyJump,缺点是你需要保持那条ssh -N隧道不中断。实际使用中我更喜欢把它和ControlMaster结合起来,比如在~/.ssh/config里配置:
Host bastion ControlMaster auto ControlPath ~/.ssh/ctl-%r@%h:%p这样第一次连接会建立主连接,后续的转发、连接都会复用,不需要反复输入密码,速度也快很多。
4.3 方案C:Remote-SSH + Remote Containers 实现“附加到容器”
如果你已经通过 VSCode Remote SSH 连接到了宿主机,那么还有一种更轻量的方式:安装 Remote Containers 扩展,直接“附加”到正在运行的容器里。具体操作是:
- 用 Remote-SSH 连接目标服务器(宿主机)。
- 在扩展市场安装“Remote Containers”(如果远程环境里没有,需要先装到远程主机)。
- 命令面板输入
Remote-Containers: Attach to Running Container...。 - 选择目标容器,VSCode 会自动进入容器环境。
这个方案不需要容器里有 SSHD,不需要额外端口映射,直接复用 Docker 的 API 连接到容器进程,查看容器内文件系统、运行终端、调试代码都非常方便。它和“先在宿主机下载 vscode-server,再进容器下载一份 server”的原理类似,但整体体验是最接近“在容器里开发”的。
需要注意:远程宿主机上必须能执行 docker 命令,Remote Containers 底层是通过 Docker CLI 与容器通信的,如果当前用户不在 docker 用户组里,需要先解决权限问题,否则会报 “permission denied while trying to connect to the Docker daemon socket”。
三种方案我实际都用过。如果只追求快速看代码,方案C体验最好;如果你需要把容器当作独立开发环境,或者想让外部的 CI/终端工具也能直接连进容器,方案A更通用;方案B适合临时调试,用完随时断开隧道,不污染任何配置。
5. 常见问题与排查笔记
5.1 最常踩的坑:vscode-server 下载卡住
无论有没有跳板机,VSCode Remote SSH 连远程主机时最容易卡住的地方就是 vscode-server 下载。症状是连接窗口一直显示“Setting up SSH Host”,十几分钟后还是老样子。排查思路按照优先级来:先登录跳板机,再从跳板机登录目标机,手动确认目标机到外网的连通性。很多内网服务器只开放了 80/443 端口,可能无法直连 VSCode 的更新服务,这种情况下把remote.SSH.allowLocalServerDownload设置为 true,让本地机器代下载即可。
提示:跳板机场景下,vscode-server 下载的是“最终目标机”上的版本,如果目标机无法访问外网,和跳板机有没有外网无关,先明确这一点能少走很多弯路。
5.2 跳板机私钥与目标机私钥如何区分
我见过不少人把同一把私钥同时放到跳板机和目标机里,最终导致本地 SSH 连接时拿到一堆认证失败日志。正确理解是:本地到跳板机的认证使用Host bastion配置里的IdentityFile,跳板机到目标机的认证使用Host prod-web配置里的IdentityFile。这两个文件最好分开生成,目标机的私钥不要直接放在跳板机上,否则跳板机本身就能直接登录目标机,安全边界形同虚设。
5.3 不打开 VSCode 时 SSHD 日志怎么看
如果 SSH 连接本身失败,VSCode 只会给出很模糊的报错。此时应该回到终端,先执行ssh -vvv prod-web,从详细输出里看认证到哪一步失败。如果跳板机转发正常,但目标机拒绝密钥,大概率是目标机authorized_keys文件权限或内容有问题。也可以在目标机上临时开启 sshd 调试日志:
sudo tail -f /var/log/auth.log重新执行一次连接,看日志输出会非常直观。有个容易被忽略的点:目标机的 OpenSSH 版本如果太老,默认可能不加载ed25519类型的密钥,确认一下服务器支持哪种密钥格式。
5.4 双跳、多级跳板也能搞定
有些网络架构不止一台跳板机,比如本地先到 DMZ 区的一台机器,再到核心内网的机器,最后才到目标服务器。ProxyJump支持逗号分隔多个跳板:
Host core-server HostName 10.20.1.5 User admin ProxyJump dmz-zone,core-zone执行连接时,OpenSSH 会先连dmz-zone,再通过它连core-zone,最后从core-zone连目标机。配置越链越长时,建议在每台跳板机的条目上也加ServerAliveInterval,因为中间任何一跳掉线,整条链路都会断。
5.5 有没有必要上专业的跳板机系统
如果团队规模小,用普通服务器当跳板机完全够用。但如果涉及大量审计需求,比如谁在什么时候执行了什么命令、上传下载过什么文件,那么建议考虑专业堡垒机方案。这类系统通常自带 Web 入口、审计录像和命令记录,并且会提供统一的 SSH 接入方式,和 VSCode Remote SSH 配合时,本质上只要把跳板地址换成堡垒机地址即可,只是有些堡垒机会强制要求二次认证,这会导致 VSCode 的首次连接多一步人工确认,需要稍微设置一下认证方式。
配置跳板机连接这件事,归根结底就是理解“本地 -> 跳板 -> 目标机”这条链路,并用 SSH 配置文件把链路固化下来。我在实际使用中有一个小习惯:每新增一个目标服务器,都在~/.ssh/config里复用同一个跳板别名,并且给目标机设置一个清晰易记的名字,这样 VSCode 的主机列表永远只显示业务别名,不会出现一堆看不懂的 IP。如果你以后遇到“明明能连跳板,却连不上目标机”的情况,先别急着怪 VSCode,回终端用ssh -vvv跑一遍,多半能定位到密钥、转发开关或者网络策略这几个点上。