公众号旧文

多服务器做主备主控,免密 SSH 的注意事项

作者:胖子阅读 —

#终于可以关掉密码登录

当服务器数量从几台增长到十几台、几十台之后,单靠记住每台服务器密码,然后逐台登录的方式很快就会失控。

更合理的做法,是建立一台统一的主控服务器,通过内网 IP、Tailscale、WireGuard 或其他私网方式,集中 SSH 登录所有节点,再通过统一的 hosts.txt 和批量脚本做日常运维。

但只配置一台主控还不够。

真正稳定的运维体系,至少应该解决两个问题:

  1. 主控本身不能成为单点故障
  2. 所有管理入口都应该使用独立、可撤销的 SSH 密钥

这次实际搭建下来,最终形成了一套比较清晰的结构。


一、主控服务器一定要有主备

最开始的架构很简单:

JP-IIJ
   ↓
所有服务器

JP-IIJ 上维护:

/opt/cluster/hosts.txt
/opt/cluster/scripts/

并且可以免密 SSH 登录 hosts.txt 中的全部服务器。

这种方式日常使用非常方便。

例如批量检查:

cd /opt/cluster
./scripts/check-cluster.sh

就可以一次查看所有服务器的:

  • hostname
  • 时区
  • vnStat
  • Tailscale 版本
  • 磁盘占用
  • 内存使用

问题在于:

如果 JP-IIJ 某一天失联怎么办?

如果所有集中管理能力都依赖这一台服务器,那么主控自己就成了最大的单点故障。

因此更合理的架构应该是:

┌── JP-IIJ 主控
              │
管理体系 ──────┤
              │
              └── JP-PRO 备用主控

两台机器都可以独立访问全部服务器。

最终效果:

JP-IIJ → 全部节点
JP-PRO → 全部节点

这样即使 JP-IIJ 完全失联,也只需要登录 JP-PRO:

ssh root@JP-PRO
cd /opt/cluster

就能继续进行集群管理。


二、备用主控不要复制主控私钥

这里有一个很重要的安全原则。

最省事的做法显然是:

把 JP-IIJ 的 SSH 私钥复制到 JP-PRO

这样两台机器马上都能登录所有服务器。

但不推荐这么做。

更好的方式是:

JP-IIJ
   └── Key A

JP-PRO
   └── Key B

两台主控各自生成独立密钥。

例如 JP-PRO:

ssh-keygen \
  -t ed25519 \
  -f /root/.ssh/id_ed25519_cluster \
  -N '' \
  -C 'jp-pro-cluster-admin'

然后只把:

id_ed25519_cluster.pub

下发到其他服务器。

私钥:

id_ed25519_cluster

永远只留在 JP-PRO 本机。

这样有一个非常现实的好处:

如果 JP-PRO 某一天被入侵,只需要从所有服务器的:

/root/.ssh/authorized_keys

删除:

jp-pro-cluster-admin

对应的公钥即可。

JP-IIJ 完全不受影响。

如果两台机器共用同一把私钥,就无法做到这种精细撤权。


三、备用主控最好同步完整运维目录

只配置 SSH 还不算真正的备用主控。

如果 JP-IIJ 上有:

/opt/cluster/

其中包含:

hosts.txt
scripts/
audit/
backup/
README.md
upgrade-tailscale.sh
find-cert.sh
search-history.sh

那么最好直接同步到备用主控。

例如:

scp -r /opt/cluster/ root@100.64.0.20:/opt/

这样 JP-PRO 不只是“能 SSH 到其他机器”,而是连运维工具、脚本、主机清单也都有一份。

真正验证备用主控是否可用,不应该只测试:

ssh root@100.64.0.3

而应该直接跑一遍真实脚本:

cd /opt/cluster
./scripts/check-cluster.sh

只有当它可以正常遍历全部节点,才说明备用主控是真的可以接管。


四、最好再保留本地 Windows 管理入口

主备服务器都在云上,仍然不代表万无一失。

更稳妥的结构是:

JP-IIJ
JP-PRO
Windows A
Windows B

四个管理入口分别拥有独立密钥。

例如:

JP-IIJ       → Key A
JP-PRO       → Key B
Windows A    → Key C
Windows B    → Key D

这样即使两台云主控都发生问题,只要自己的 Windows 还能连接 Tailscale,就仍然可以直接:

ssh root@100.64.0.15

进入任意服务器。

这实际上已经从“主备”进一步变成了:

多入口管理

可靠性会高很多。


五、批量配置免密登录时,最容易踩的坑

免密 SSH 本身并不复杂,但批量操作时有几个细节非常容易出问题。

1. 每台管理设备使用独立密钥

这是最重要的一条。

不要把同一把私钥复制到:

主控 A
主控 B
Windows A
Windows B

应该分别生成:

jp-iij-admin
jp-pro-cluster-admin
windows-cluster-admin
home-windows-cluster-admin

这样以后哪台设备丢失,就只撤销哪一把 key。


2. 只复制公钥,不复制私钥

服务器之间需要分发的是:

id_ed25519.pub

或者:

id_ed25519_cluster.pub

不是:

id_ed25519

私钥应该永远只保留在发起 SSH 登录的那台设备。


3. authorized_keys 要避免重复写入

最简单的追加方法:

cat id_ed25519.pub >> /root/.ssh/authorized_keys

能用,但如果重复执行很多次,会出现重复公钥。

更稳妥的是:

PUBKEY=”$(cat id_ed25519.pub)”

grep -qxF ”$PUBKEY” /root/.ssh/authorized_keys ||
echo ”$PUBKEY” >> /root/.ssh/authorized_keys

这样即使脚本重复执行,也不会重复插入。


4. 权限必须正确

OpenSSH 对权限比较敏感。

推荐统一:

chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
chmod 600 /root/.ssh/id_ed25519

如果权限过宽,有些 SSH Server 会直接拒绝使用公钥。


5. while read 中调用 SSH 要加 -n

这是批量脚本里一个很隐蔽的问题。

错误写法:

while read -r host; do
    ssh ”$host” hostname
done < hosts.txt

第一台执行完后,循环可能直接结束。

原因是 SSH 继承了 stdin,把后面的 hosts.txt 内容读掉了。

正确写法:

while read -r host; do
    ssh -n ”$host” hostname
done < hosts.txt

或者:

ssh ”$host” hostname </dev/null

批量 SSH 时最好养成默认加 -n 的习惯。


六、Host Key 和登录密钥是两回事

很多人第一次批量配置时会混淆两个概念:

SSH Host Key

和:

SSH User Key

它们不是同一件东西。

用户密钥决定:

你有没有权限登录服务器。

Host Key 决定:

你连接的到底是不是原来的那台服务器。

因此即使公钥配置完全正确,也可能遇到:

Host key verification failed

或者:

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

这种错误和登录私钥本身没有关系。

例如服务器重装后,SSH Host Key 改变,本地 known_hosts 仍保存旧值,就会拒绝连接。

确认服务器身份确实正确后,可以:

ssh-keygen -R 100.64.0.3

然后重新建立连接。

推荐使用:

StrictHostKeyChecking=accept-new

而不是:

StrictHostKeyChecking=no

accept-new 会自动接受第一次出现的新主机,但如果已经记录的主机指纹发生变化,仍然会报警。

安全性更合理。


七、不要把“批量写入成功”当成“免密登录成功”

在批量下发公钥时,可能看到:

OK
OK
OK
OK

但这只能说明:

SSH 到服务器并执行写入命令成功。

并不能证明:

新生成的那把私钥真的可以登录服务器。

因此必须做第二轮验证。

例如:

ssh \
  -i /root/.ssh/id_ed25519_cluster \
  -o IdentitiesOnly=yes \
  root@100.64.0.3 hostname

其中:

-i

明确指定新私钥。

而:

IdentitiesOnly=yes

可以防止 SSH 偷偷尝试其他已有密钥。

这样才能真正确认:

当前成功登录用的,就是刚刚配置的那一把 key。


八、批量测试一定要考虑单台机器卡死

ConnectTimeout=5 只保证连接阶段不会无限卡住。

它不保证:

SSH 建连之后

远程命令也一定按时返回。

所以批量检测时,最好再加一层外部超时。

Windows PowerShell 可以使用:

Start-Job
Wait-Job -Timeout 10

这样某一台机器即使异常,也只会显示:

TIMEOUT

而不会阻塞后面所有服务器。

Linux 上也可以使用:

timeout 10 ssh ...

避免“一台服务器拖死整批任务”。


九、最好通过 SSH config 固定管理密钥

如果密钥文件名不是默认的:

id_ed25519

而是:

id_ed25519_cluster

那么每次都写:

ssh -i ~/.ssh/id_ed25519_cluster root@100.64.0.15

会很麻烦。

更合适的是配置:

~/.ssh/config

例如:

Host 100.64.0.*
    User root
    IdentityFile ~/.ssh/id_ed25519_cluster
    IdentitiesOnly yes

以后直接:

ssh root@100.64.0.15

SSH 会自动选择正确的管理密钥。

如果某一台服务器使用特殊 key,也可以单独覆盖:

Host 100.64.0.55
    User root
    IdentityFile ~/.ssh/id_ed25519_special
    IdentitiesOnly yes

Host 100.64.0.*
    User root
    IdentityFile ~/.ssh/id_ed25519_cluster
    IdentitiesOnly yes

具体规则放前面,通用规则放后面即可。


十、最终推荐的管理架构

一套比较合理的小规模服务器管理体系可以是:

┌── JP-IIJ
                    │
                    ├── JP-PRO
管理员 ──────────────┤
                    ├── Windows TTX
                    │
                    └── Windows ABCD
                           │
                           ↓
                     Tailscale 私网
                           │
                           ↓
                    所有服务器节点

每个入口:

独立 SSH 私钥
独立公钥身份
独立撤销能力

主控服务器之间:

都维护 hosts.txt
都保留 /opt/cluster
都能运行批量脚本

最终目标不是:

“有一台机器可以登录所有服务器。”

而应该是:

“任何一个主要管理入口失效,都不会让我失去整个集群的管理能力。”

这才是主备主控真正解决的问题。

总结

多服务器集中运维最值得坚持的两个原则其实很简单。

第一:

主控本身也必须做冗余。

不要让负责管理全部服务器的机器,反而成为最危险的单点故障。

第二:

免密 SSH 应该按设备独立发放密钥,而不是到处复制同一把私钥。

配合 authorized_keys、known_hosts、IdentityFile、IdentitiesOnly、连接超时以及真实登录验证,就可以构建一套既方便,又比较容易维护和撤权的服务器管理体系。