内网服务器没有公网 IP,外网客户端无法直接连接,但内网服务器可以主动访问公网。这时,可以让内网服务器向一台公网跳板机建立反向 SSH 隧道,再让外网客户端通过 ProxyJump 使用这条隧道登录内网服务器。
本文以使用 systemd 的 Ubuntu / Debian 主机为例。最终只需要在外网客户端执行 ssh internal-via-jump,不需要在内网路由器上配置入站端口映射,也不需要把内网 SSH 端口直接开放到公网。
先理清连接方向与认证关系
三台机器的角色如下。后文中的地址和用户名都是示例,执行前请替换为实际值。
| 角色 | 示例 | 职责 |
|---|---|---|
| 外网客户端 | 你的电脑 | 发起最终的 SSH 登录 |
| 公网跳板机 | jump.example.com,用户 jumpbox | 接收隧道连接,并提供回环监听端口 2252 |
| 内网服务器 | 用户 internal_user,SSH 端口 22 | 主动建立隧道,也是最终登录目标 |
整个过程分成两步:
1. Establish the reverse tunnel:
Internal server -- outbound SSH --> Jump host
Jump host 127.0.0.1:2252 -- tunnel --> Internal server 127.0.0.1:22
2. Log in through the jump host:
Client -- ProxyJump --> Jump host 127.0.0.1:2252
-- reverse tunnel --> Internal server 127.0.0.1:22
转发流量与登录认证是两件事。 ProxyJump 在跳板机上建立 TCP 转发,最终登录内网服务器的 SSH 客户端仍然运行在你的电脑上。因此,需要配置三条公钥授权关系:
| 发起连接的一方 | 认证目标 | 应添加到目标 authorized_keys 的公钥 |
|---|---|---|
| 内网服务器上的隧道进程 | 跳板机的 jumpbox 用户 | 内网服务器的隧道专用公钥 |
| 外网客户端 | 跳板机的 jumpbox 用户 | 外网客户端的公钥 |
| 外网客户端 | 内网服务器的 internal_user 用户 | 外网客户端的公钥 |
跳板机不需要持有能登录内网服务器的私钥,也不需要开启 agent forwarding。最终会话的 SSH 加密和认证发生在外网客户端与内网服务器之间。
第一步:准备软件和密钥
开始前,确认以下条件成立:
- 内网服务器运行 SSH 服务,并且能访问跳板机的 SSH 端口。
- 外网客户端也能访问跳板机的 SSH 端口;本文假设该端口为
22。 - 跳板机和内网服务器的管理员能通过已有登录方式或控制台配置用户及公钥。
在内网服务器安装 autossh:
sudo apt update
sudo apt install autossh
command -v autossh
后面的服务文件使用 /usr/bin/autossh;如果实际安装路径不同,需要同步修改。
在外网客户端准备登录密钥。如果已有合适的密钥,可以直接使用;否则执行以下命令并设置口令。若提示文件已存在,不要覆盖原密钥:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "client-login"
在内网服务器上,以之后运行 systemd 服务的 internal_user 用户生成隧道专用密钥:
install -d -m 700 ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_jump_tunnel -C "internal-reverse-tunnel"
隧道需要在无人值守时完成认证。本文示例采用不设置口令的专用密钥:生成时在口令提示处直接回车,并保护好私钥文件及该用户账号。如果采用带口令的私钥,需要额外配置服务可用的 agent;systemd 系统服务不会自动继承终端中的 ssh-agent 环境。
公钥是 .pub 文件,私钥是不带 .pub 后缀的文件。后续只复制公钥。
第二步:在跳板机配置授权和转发
创建用户并添加两个公钥
在跳板机上创建普通用户;如果用户已经存在,跳过创建命令:
sudo adduser --disabled-password --gecos "" jumpbox
sudo -iu jumpbox
install -d -m 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
vim ~/.ssh/authorized_keys
这里使用 sudo -iu jumpbox,不需要知道新用户的密码。将下面两个公钥分别添加为一行,保留文件中已有的其他授权:
- 外网客户端的
~/.ssh/id_ed25519.pub。 - 内网服务器的
~/.ssh/id_ed25519_jump_tunnel.pub。
这些命令在 jumpbox 用户下执行,文件自然归该用户所有。如果通过 root 复制文件,还需要检查 .ssh 和 authorized_keys 的属主。完成后执行 exit,回到原来的管理员账号,再检查服务端配置。
确认转发权限与监听范围
在跳板机的 SSH 服务端配置中,确认针对 jumpbox 用户的有效策略允许 TCP 转发,并保持反向端口仅在回环地址监听。相关设置为:
AllowTcpForwarding yes
GatewayPorts no
-R 使用远程转发,而 ProxyJump 需要跳板机允许客户端请求 TCP 转发,因此不能只允许其中一个方向。已有的 Match 规则、DisableForwarding 或公钥选项也可能限制转发;应检查实际生效的配置,不要只在文件末尾机械追加。
如果修改了服务端配置,先执行 sudo sshd -t 检查语法,再在 Ubuntu / Debian 上执行 sudo systemctl reload ssh。保留当前管理会话,另开连接确认没有影响已有登录。
无需在安全组或防火墙中向公网开放 2252。 外网客户端先登录跳板机,再由跳板机连接自己的回环端口。GatewayPorts no 会将远程转发限制在回环地址;如果设为 yes,服务端会强制使用通配地址监听,即使客户端显式指定了回环地址,也不能依赖它限制暴露范围。
专用普通用户便于管理,但默认仍可执行命令,也可能转发到其他地址。多人共用跳板机时,可以进一步拆分隧道账号和登录代理账号,并按用途设置 PermitListen、PermitOpen 等限制。回环监听也不等于用户隔离:跳板机上的其他本地用户仍可能连接 2252,最终访问权限由内网 SSH 认证控制。
第三步:在内网服务器授权外网客户端
以 internal_user 身份在内网服务器执行:
install -d -m 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
vim ~/.ssh/authorized_keys
添加外网客户端的 ~/.ssh/id_ed25519.pub,不是跳板机用户的公钥。
内网服务器建立隧道时,是它向跳板机证明身份;外网客户端经隧道登录时,则是外网客户端向内网服务器证明身份。这两次认证使用不同的密钥和授权文件。
第四步:先在前台验证反向隧道
在内网服务器上,以 internal_user 身份先进行一次普通 SSH 登录:
ssh -i ~/.ssh/id_ed25519_jump_tunnel \
-o IdentitiesOnly=yes jumpbox@jump.example.com
首次连接时,通过可信渠道核对跳板机的主机密钥指纹,再确认并写入该用户的 known_hosts。用户公钥授权与主机身份校验各有用途:authorized_keys 决定谁可以登录,known_hosts 帮助确认连接的是哪台服务器。
确认能使用密钥登录后,输入 exit 返回内网服务器,再启动前台隧道:
autossh -M 0 -N \
-i /home/internal_user/.ssh/id_ed25519_jump_tunnel \
-o IdentitiesOnly=yes \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
-R 127.0.0.1:2252:127.0.0.1:22 \
jumpbox@jump.example.com
如果用户的家目录不是 /home/internal_user,请修改私钥路径。这个命令会持续占用终端,没有输出并不意味着启动失败。
| 参数 | 含义 |
|---|---|
-M 0 | 关闭 autossh 的监控端口;依靠 SSH 进程退出触发重连 |
-N | 不执行远程命令,只做转发 |
-i、IdentitiesOnly=yes | 指定隧道使用的身份密钥 |
BatchMode=yes | 禁止密码和确认提示等交互,便于无人值守运行 |
StrictHostKeyChecking=yes | 只连接主机密钥已受信任且未发生不匹配变化的服务器 |
ExitOnForwardFailure=yes | 无法建立请求的端口转发时退出,例如 2252 已被占用 |
ServerAliveInterval=60 | 连续 60 秒没有收到服务端数据时,通过加密通道请求响应 |
ServerAliveCountMax=3 | 达到连续未响应阈值后退出,让 autossh 有机会重建连接 |
-R 127.0.0.1:2252:127.0.0.1:22 中,第一个地址是跳板机的监听地址,第二个地址是从内网服务器一侧访问的目标地址。收到转发流量后,隧道客户端会连接内网服务器自身的 127.0.0.1:22。
-M 0 不会自动开启 SSH 心跳,所以需要显式设置上述 ServerAlive* 参数。在已经建立连接、服务端持续不响应的情况下,这组设置通常会在约 180 秒后让 SSH 退出;它不是整体恢复时长的保证。
此处刻意使用前台运行,方便检查错误并停止测试。autossh -f 会让 autossh 自身先进入后台,SSH 无法再交互询问密码或私钥口令;首次验证不应直接使用它。
第五步:配置外网客户端并完成登录
保持前台隧道运行,在外网客户端的 ~/.ssh/config 中添加:
Host jumpbox-public
HostName jump.example.com
User jumpbox
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3
Host internal-via-jump
HostName 127.0.0.1
Port 2252
User internal_user
ProxyJump jumpbox-public
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
HostKeyAlias internal-via-jump
ServerAliveInterval 60
ServerAliveCountMax 3
先验证跳板机登录,再验证完整链路:
ssh jumpbox-public
# 确认登录成功后执行 exit,返回外网客户端
ssh internal-via-jump
两次首次连接都应核对相应服务器的主机密钥指纹:第二次看到的是内网服务器的指纹。
这里的 HostName 127.0.0.1 是通过 ProxyJump 从跳板机一侧连接的目标地址,不是外网客户端自己的回环地址。跳板机收到对 127.0.0.1:2252 的连接后,再通过已建立的反向隧道把流量交给内网服务器。
HostKeyAlias 为这个目标指定独立的主机密钥识别名称,避免管理多个类似 127.0.0.1:2252 的目标时混淆主机记录。每台内网服务器应使用不同的别名;同一跳板机上的多个反向隧道也需要不同监听端口。
跳板机和最终目标分别设置连接参数,因为最终目标的 SSH 配置通常不会自动应用到跳板机连接。这个流程不需要在跳板机上再执行一次 ssh,也不需要复制外网客户端的私钥。
第六步:交给 systemd 长期运行
完整登录成功后,在内网服务器的前台隧道终端按 Ctrl+C,停止测试实例。不要让手动实例和 systemd 服务同时争用 2252。 如果此前使用过后台实例,也应确认并停止对应的 autossh 进程。
在内网服务器创建 /etc/systemd/system/autossh-tunnel.service:
[Unit]
Description=Reverse SSH tunnel to jump host
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=internal_user
Environment=AUTOSSH_GATETIME=0
ExecStart=/usr/bin/autossh -M 0 -N -i /home/internal_user/.ssh/id_ed25519_jump_tunnel -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -o ConnectTimeout=10 -R 127.0.0.1:2252:127.0.0.1:22 jumpbox@jump.example.com
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
将用户名、家目录、跳板机地址和 autossh 路径替换为实际值。服务必须使用前面完成密钥登录和主机指纹确认的用户,否则可能找不到正确的私钥或 known_hosts。
这份配置中:
network-online.target用于等待启动阶段的网络配置就绪;具体含义取决于网络管理服务及 wait-online 配置,不能保证公网或跳板机可达,也不会持续监控网络。AUTOSSH_GATETIME=0关闭 autossh 对首次启动快速失败的提前退出处理,使它可以继续重试。ConnectTimeout=10限制单次 SSH 建连及初始握手的等待时间,不是整个重连过程的超时。- autossh 负责重启退出的 SSH 子进程;systemd 的
Restart=always负责在 autossh 自身退出后重新启动服务。连续连接失败时,autossh 还会退避,因此RestartSec=10不代表每 10 秒必定重建一次隧道。 - 服务内不使用
-f,让 systemd 直接跟踪前台运行的 autossh 进程。
加载并启动服务:
sudo systemctl daemon-reload
sudo systemctl enable --now autossh-tunnel.service
sudo systemctl status autossh-tunnel.service
sudo journalctl -u autossh-tunnel.service -n 50 --no-pager
修改已经运行的服务文件后,执行 daemon-reload,再执行 sudo systemctl restart autossh-tunnel.service,新配置才会应用到进程。
如何确认可用,以及如何排障
不要仅凭 systemctl status 显示 active 就判断隧道可用。autossh 可能正在等待重试,而监听成功也不代表最终 SSH 服务正常。
建议依次检查:
- 内网服务器上的 SSH 服务:在内网服务器运行
ss -ltn 'sport = :22',确认 SSH 监听覆盖127.0.0.1:22。 - 跳板机上的反向监听:在跳板机运行
ss -ltn 'sport = :2252',确认监听地址为127.0.0.1:2252,而非0.0.0.0:2252或[::]:2252。 - 完整登录链路:在外网客户端执行
ssh internal-via-jump,确认登录到预期内网服务器。 - 恢复行为:在允许中断测试的环境中重启隧道服务,确认能重新建立连接。隧道恢复后需要重新登录,已经断开的交互会话不会自动续接。
常见故障可以按下面的方向定位:
| 现象 | 优先检查 |
|---|---|
Permission denied (publickey) | 用 ssh -v 判断是哪一次认证失败,再检查对应用户、公钥、私钥路径及文件属主和权限 |
Host key verification failed | 检查实际运行用户的 known_hosts;核对指纹及主机变更原因,不要直接关闭校验 |
remote port forwarding failed | 检查 2252 是否被旧隧道占用,以及服务端是否允许远程转发 |
administratively prohibited | 检查跳板机上的转发策略、Match 规则和公钥限制 |
Connection refused | 检查跳板机 2252 是否监听,以及内网服务器的 127.0.0.1:22 是否可达 |
| 服务运行但无法登录 | 查看服务日志,再逐段检查隧道、目标 SSH 服务和最终用户认证 |
ExitOnForwardFailure=yes 能发现监听建立失败,但不能发现之后转发目标的连接失败。例如内网 SSH 服务停止时,隧道进程可能仍然正常运行,autossh 不会因此自动修复目标服务。
这个方案适用于内网能够主动访问公网跳板机的场景。它能恢复中断的隧道,但可用性仍取决于网络、跳板机和内网 SSH 服务;安全边界则依赖主机身份校验、私钥保护与各端的授权策略。