来源:
notes/documents/跨跳板机操作内网主机的_SSH_双层跳转方法.md跨跳板机操作内网主机的 SSH 双层跳转方法
场景
需要操作一台位于远程内网、没有公网出口的虚拟机。能直达的第一跳只有一台跳板机(飞牛 fnOS),目标虚拟机在它的局域网段里,只能从跳板机再跳一次。
这类「两层 SSH」的场景在运维里很常见,但直接写嵌套命令会立刻撞上两个问题:密码没法交互输入,以及引号被外层 shell 提前吃掉。
整体链路
本机
│ 第 1 跳:SSH,用 SSH_ASKPASS 自动喂密码(本次走 ZeroTier)
▼
跳板机 10.71.48.167 / 192.168.110.147
│ 第 2 跳:跳板机自带 sshpass,SSH/SCP 到内网
▼
目标虚拟机 192.168.110.91
第 1 跳:用 SSH_ASKPASS 实现非交互密码登录
自动化环境里没有 tty,ssh 弹不出密码提示,会直接失败。这时用 SSH_ASKPASS:让 ssh 去执行一个脚本拿密码。
# askpass 脚本的内容就是打印密码
printf '#!/bin/sh\nprintf "%%s\\n" "密码"\n' > /tmp/.rap.sh
chmod 700 /tmp/.rap.sh
export SSH_ASKPASS=/tmp/.rap.sh
export SSH_ASKPASS_REQUIRE=force # 关键,OpenSSH 8.4+ 才支持
export DISPLAY=:0 # 不设 DISPLAY 时可能不会启用 askpass
setsid -w ssh -n ... admin@跳板机 '命令'
三个细节:
SSH_ASKPASS_REQUIRE=force是重点。默认情况下 ssh 只在「完全没有 tty」时才走 askpass,force让它无条件走。setsid用于脱离当前控制终端,正好制造「没有 tty」的条件;-w保证等待子进程结束再返回。-n把 stdin 重定向到/dev/null,避免 ssh 反过来抢 stdin。
第 2 跳:复用跳板机上现成的 sshpass
先探测目标环境,发现跳板机自带 /usr/bin/sshpass,不需要额外安装:
export SSHPASS='密码'
sshpass -e ssh -n -o ... admin@目标机 '命令'
用 sshpass -e(从 SSHPASS 环境变量读密码)而不是 sshpass -p 密码。区别很实际:-p 会把密码写进命令行参数,任何用户 ps 一下就能看到;-e 不会。
关键技巧:base64 包装内层命令
命令要穿过两层 shell,引号和 $ 会被外层提前解释掉:
ssh 跳板机 "ssh 目标机 \"echo 密码 | sudo -S -p '' id\"" # 转义地狱
解决办法是把最内层要执行的脚本先 base64 编码,变成一个不含任何特殊字符的字符串,到对面再解码执行:
B64=$(base64 -w0 < 命令)
ssh 跳板机 "echo $B64 | base64 -d | bash"
base64 字符集只有 A-Za-z0-9+/=,无论套几层 SSH 都不会被改写。这样就能安全传递任意多行脚本,包括带单双引号、$()、heredoc 的内容。这是整个方案里最省事的一步。
封装成可复用脚本
把机制固化成两个小脚本,都从 stdin 读命令、自行 base64、再投递:
- 单跳脚本:到跳板机
- 双跳脚本:到内网虚拟机(在第 1 跳里再嵌第 2 跳)
使用方式:
cat <<'PAYLOAD' | ./vmrun.sh
hostname
ls -lh /opt/zerotier/bin/zerotier-one
PAYLOAD
在目标机上提权
目标机的 sudo 需要密码,而 stdin 已经被 base64 管道占用,所以单独喂一次:
echo '密码' | sudo -S -p '' bash 要执行的脚本
-S 从 stdin 读密码,-p '' 清空提示符,避免提示文字混进命令输出。
凭据收尾
所有临时凭据(askpass 脚本、SSHPASS、known_hosts)在每轮操作结束后立即删除,不留在磁盘上。
为什么不用 ProxyJump
ssh -J admin@跳板机 admin@目标机(ProxyJump)本来可以一次穿透,写法更短。没有采用的原因:
- 两跳的密码提示会混在一起,在非 tty 环境下更难控制
- 跳板机本来就有
sshpass,分层写更直观 - 分层之后,哪一跳出问题一眼就能定位,排障成本更低
复用要点
- 自动化环境里 ssh 登录优先用
SSH_ASKPASS+setsid,比sshpass更通用(不依赖额外软件),但要满足SSH_ASKPASS_REQUIRE=force和DISPLAY两个前提。 - 跨多层 shell 传命令,优先用 base64 包装,能直接消灭转义问题,比拼引号可靠得多。
- 优先用
sshpass -e而不是-p,避免密码出现在进程列表里。 - 先探测跳板机有什么能力(
command -v sshpass、ping是否可用等),往往能省掉安装步骤。这次也发现跳板机的ping缺cap_net_raw,只能改用 TCP 探测。 - 把机制固化成脚本再迭代。这次因为要反复「执行 → 报错 → 修复」,封装脚本让每轮重试只花几秒,而不是重敲一长串 ssh 参数。
相关
本文是 ZeroTier 部署过程的方法提炼,部署细节见 notes/documents/通过跳板机把_Deepin_25_虚拟机接入_ZeroTier_网络.md。