// LATEST

【技术分享】一个字母管住所有 SSH:我写了个叫 s 的连接管理器

一个字母管住所有 SSH — s 连接管理器

当服务器从 3 台变成 30 台,ssh 这件事就开始变得难受了。IP 记不住,密钥和密码混着来,有的机器登进去还得手动 sudo -i,换台电脑连密码都得重配。我一直想要一个足够简单的命令:记住一个短别名、甚至什么都不用记,敲一下就进去

于是有了 s——一个 bash 脚本,把日常 SSH 运维压缩成一个字母开头的命令。这篇文章记录它的设计取舍、完整功能,以及几个写它时踩到的坑。

一分钟感受

直接敲一个 s,弹出一个交互菜单:所有机器按分组列好,打字就地模糊搜索,回车即连。

裸敲 s 打开交互菜单

分组标题用一条横线拉到定宽、机器缩进挂在下面,层次一眼看清;别名按登录方式着色——青色是密钥登录,琥珀色是密码登录;打字时别名、user@host、描述、分组名全都能搜,搜 iai 就只剩那一组,搜某段 IP 直接定位到那台。

记得住别名的话,更快:

s hk-01                    # 直接连,密钥/密码自动判断
s ping                     # 全部主机一眼看在线/离线
s run 'db-*' 'df -h /'     # 一批机器上跑同一条命令
s export ~/s-backup.enc    # 打成加密备份,换电脑一条 s import 全恢复

一个核心决定:不发明新格式,就用 ~/.ssh/config

市面上的 SSH 管理器大多有自己的一套配置文件/数据库。我不想要——那意味着 scprsync、VS Code Remote 这些工具都得再教一遍。

s 的所有主机就是标准的 ~/.ssh/config 条目

Host hk-01
  HostName 1.2.3.4
  User root
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes

好处是白拿的:

  • s hk-01 能连,原生 ssh hk-01 也能连;
  • scp file hk-01:/tmp/rsync ... hk-01:... 全部自动认识这个别名;
  • 别的工具、别的机器只要有这份 config 就通用,s 只是个更顺手的入口,不是一道锁。

那些 s 自己需要、而 SSH 又不认识的元数据(分组、描述、登录方式、是否自动切 root),我用注释行存在同一个 Host 块里:

Host win-box
  #s-group  跳板
  #s-desc   跳板机
  #s-auth   password
  #s-root   yes
  HostName  9.9.9.9
  User      admin

ssh 会忽略所有 # 开头的行,所以这些标记对原生 ssh 完全透明,而 sawk 把它们读出来。一份配置,两套解读,互不打架:

                    ~/.ssh/config
              ┌───────────────────────┐
              │ Host win-box          │
    #s-* 注释 │   #s-group  跳板       │ ← ssh 视而不见
    只有 s 读 │   #s-auth   password  │   (# 开头一律忽略)
              │   #s-root   yes       │
              │   HostName  9.9.9.9   │ ← ssh 和 s 都读
    标准字段  │   User      admin     │
              └───────────┬───────────┘
                          │
              ┌───────────┴───────────┐
              ▼                       ▼
        原生 ssh / scp            s(多读几行注释,
        rsync / VS Code           知道要分组、走密码、切 root)
        (只认标准字段)

列表、分组、批量改字段

裸敲 s 进的是交互菜单;想要一份纯文本清单,s ls 按分组列出全部:

s ls 按分组列出

机器一多,平铺一长条看着乱,所以有了分组。分组信息就是前面那行 #s-group <组名>s group <别名> <组名> 设置。但十几台一台台敲太蠢,于是让它跟 s ping / s run 一样支持通配符和逗号选择器批量操作:

通配符批量归组

s set 'lbc-*' group LBC 一条命令就把所有 lbc- 开头的机器归到一组。顺带一提,s set 现在也接管了 group / desc 字段,改 IP、改描述、改分组都能走它,还都支持通配符批量:

s set hk-01 host 1.2.3.9          # 改 IP(云主机重启后公网 IP 变了很常见)
s set 'lbc-*' port 2222           # 一批机器一起改端口
s desc 'it-*' "iai 集群"          # 描述也能批量

密钥与密码:密码不落明文

加机器就一条命令,密钥、密码都支持:

s add hk-01 root@1.2.3.4 -i ~/.ssh/id_ed25519 -d "香港 主力节点" -g 香港
s add win-box admin@9.9.9.9 --pass -d "跳板机" -g 跳板    # --pass 会提示输入密码

密钥登录没什么好说的,交给 ssh 就行。密码登录麻烦在两点:一是明文存哪,二是怎么喂给 ssh

密码存进 macOS Keychain,不落进任何明文文件。 ~/.ssh/config 里只留一行 #s-auth password 做标记,真正的密码用 security add-generic-password 写入钥匙串;连接时再 security find-generic-password 取出,通过 sshpass 喂给 ssh。配置文件即使被人翻到,也拿不到密码。

连接时 s 看到 #s-auth password 就自动走 sshpass,你完全无感:

s win-box       # 自动取出密码登录,你什么都不用输

改密码、把密码机升级成免密,也各有一条命令:

s passwd win-box     # 只更新 Keychain 里的密码
s cp win-box         # 把本机公钥拷过去(ssh-copy-id),之后就能免密了

登录后自动切 root

有些机器登进去是普通用户,第一件事永远是 sudo -i。把它交给 s

s root win-box on     # 开启:以后 s win-box 连上就自动 sudo -i

对密码机还有个细节:sudo 通常要再输一次密码,而这个密码一般和登录密码是同一个。所以 s 会把 Keychain 里那把密码顺手喂给 sudo——真正做到一条命令直达 root shell,中间一次都不用输密码。喂失败(比如两个密码确实不一样)就自动回退成手动输入,不会卡住。

s win-box
# → 自动填 SSH 密码 → 自动用同一密码过 sudo → 直接落在 root@win-box

换电脑不怕:加密备份与迁移

这是我最想要的一个功能,也是「换电脑就没了」的正解。

s 的数据其实分两处:主机定义在 ~/.ssh/config(明文,好拷),但密码机的密码在 macOS Keychain 里,跟机器绑定,不随文件走——光把 config 拷到新电脑,所有密码机都连不上。

s export 把两处一起打包:所有主机块 + Keychain 里对应的密码,用你设的口令走 openssl aes-256-cbc + pbkdf2 加密成一个 .enc 文件,全程不落明文。新电脑 s import 输口令解密,主机块 merge 进 config、密码写回 Keychain,已存在的别名自动跳过、不覆盖。

加密备份与一键恢复

这个 .enc 文件 + 口令 = 你全部服务器的凭据。别丢公共网盘,口令要记牢——解不开是没有找回的。

顺手放一张在自己终端里实际跑起来的样子(配着壁纸,IP 列打了码):

真实终端里的 s

传输、连通性、批量执行、转发

日常琐事都收敛成子命令,全部命令长这样:

s 命令速查

挑几个每天在用的:

文件传输——put/get 会自动从参数里认出 别名:路径 那部分,判断走原生 scp 还是 sshpass scp,你不用关心目标机是密钥还是密码登录:

s put ./app.tar.gz db-1:/root/    # 上传
s get db-1:/var/log/app.log ./    # 下载

连通性检查——s ping 逐台 TCP 探测,标注在线/离线,支持通配符只查一部分:

s ping                # 全部
s ping 'db-*' win-box # 只查一组

批量执行——一条命令扫一批机器,逐台执行、带标题分隔、末尾汇总成功/失败数,某台挂了不影响其它:

s run 'k8s-*' 'kubectl get nodes'
s run 'db-1,win-box' 'df -h /'

端口转发——把远端只监听在本机的服务(数据库、内部 API 等)拉到本地直连:

s fwd db-1 8123                       # 简写:本地:8123 → 远端 localhost:8123
s fwd jump 5432:10.0.0.100:5432       # 借 jump 当跳板,连它内网里的库
端口转发里最容易绕晕的一点:5432:10.0.0.100:5432 中间那个地址是从服务器角度解析的,不是你本机。写 localhost 指的是「SSH 连上的那台服务器自己」,写内网 IP 则是让那台服务器当跳板去连它内网里的第三台机器——后者能帮你够到公网根本访问不到的机器。

Tab 补全

作为一个每天要敲很多次的命令,补全是刚需。给 zsh 写了个补全函数丢进 fpath

  • s <Tab> 列出所有别名 + 子命令;
  • s hk<Tab> 补成 hk-01
  • s rm <Tab> / s group <Tab> / s ping <Tab> 补别名。

候选是实时从 ~/.ssh/config 读的——所以 s add 新加的机器会自动出现在补全里,不用维护第二份清单。

几个踩到的坑

写这种「看起来很简单」的小工具,坑往往在细节里。挑几个有代表性的。

1. 中文变量名后面跟全角括号,会报 unbound variable。 开了 set -u 后,"未找到别名:$alias(...)" 这一句直接崩:

line 147: alias�: unbound variable

原因是 bash 把 $alias 后面紧跟的全角括号 的字节吞进了变量名,去找一个不存在的变量。解法是老老实实用花括号界定:${alias}

2. 中文对齐不能用 printf %-Ns printf 按字符个数补空格,但中文在终端里占两格,于是带中文的列全歪了。得按显示宽度补齐。有个不用起子进程的小技巧——对「纯 ASCII + 三字节中文」的字符串,显示宽度正好等于 (字符数 + 字节数) / 2

dwidth() { local s="$1" c b; c=${#s}; local LC_ALL=C; b=${#s}; echo $(( (c + b) / 2 )); }

${#s} 在 UTF-8 locale 下数的是字符数,临时切到 LC_ALL=C 再数一次就是字节数,两者一平均就得到显示宽度。

3. 密码对了却 Permission denied 强制 PreferredAuthentications=password 时,某些带 PAM 的服务器其实走的是 keyboard-interactive,于是密码正确也被拒。把两种都允许即可:password,keyboard-interactive

4. set -e 会让批量执行第一台失败就整体退出。 因为 set -e 遇到非零退出就中止。解法是把 ssh 放进 if 条件里——条件语境下的命令失败不触发 set -e

if ssh ... "$alias" "$cmd"; then rc=0; else rc=$?; fi

同一个坑在写交互菜单时又踩了一次:管道左侧的 while 循环,末句如果是个求值为假的判断,整个子 shell 退出码就是非零,配合 set -o pipefail 会把外层函数一起带崩。把那种收尾判断改成 if ... fi 就好了。

5. 密码机交互登录,连上却「秒退、无输出、退出码 0」。 这个最迷惑。原因是 sshpass 介入后,ssh 的 stdin 不再被识别成终端,于是它不向远端申请伪终端;远端 shell 一起来就读到 EOF,立刻正常退出——所以退出码是 0,什么都没留下。解法是交互登录时显式 -t 强制分配伪终端:

sshpass -e ssh -t "$alias"    # 少了这个 -t,交互 shell 会瞬间关掉

6. 密码机第一次连,sshpass 直接以退出码 6 失败。 因为这台的主机公钥还不在 known_hosts 里,ssh 想弹「是否信任该主机」的确认,而 sshpass 没法回答这个交互提示,只好放弃。解法是加 StrictHostKeyChecking=accept-new:首次自动接受新主机密钥,但主机密钥若变更仍然拒绝——安全性上是个合理折中。

小结

s 没做什么高深的事,它只是把「一堆 SSH 服务器」这件事里所有重复动作——记 IP、切密钥密码、切 root、传文件、查状态、批量跑命令、分组、备份迁移——各压成一条短命令,同时坚持一个原则:底层永远是标准 ~/.ssh/config,绝不发明新格式,绝不把自己变成一道锁。

如果你也管着一堆机器,不妨拿去改。代码已经放到 GitHub,clone 下来 ./install.sh 就能用。

GitHub · almightyYantao/ssh-quick-connect

还没有评论

欢迎留下你的观点,保持交流的清晰和友好。

写下评论