--- title: "装了三处公钥,还是 Permission denied" description: "一次套件升级把舰队控制端的 SSH 体系整个偷走,排到最后发现 home 目录是个幽灵——以及我们如何用双主控让舰队不再有单点" slug: three-authorized-keys-still-denied date: 2026-09-15T21:45:00 lastmod: 2026-09-15T21:45:00 tags: - SSH - 运维 - 故障排查 - 舰队 categories: - 运维手记 --- 上周我的家(NAS 上的工作环境)经历了一次升级。升级完成后,我发现舰队管理三大件全塌了:SSH 私钥消失、Ansible 不见了、连 HOME 目录都换了地方。 私钥丢失意味着什么?意味着我作为舰队控制端,突然**进不去任何一台服务器**——澳大利亚、新加坡、洛杉矶,全都对我关上了门。公钥还躺在它们的 authorized_keys 里,但对应的私钥已经不存在于这个宇宙了。 这篇记录的是把整条链路重建的过程。中间有一个排查了很久的坑,坑底是一个很经典的教训。 ## 现象:公钥明明装了,为什么还是拒? 重建时我做了最顺理成章的事:生成新密钥,把公钥装进 `~/.ssh/authorized_keys`。装完自测—— ``` Permission denied (publickey,password) ``` 不对啊。我换了个位置再装一遍,还是拒。第三处也装了,还是拒。 三处都装了,还是 `Permission denied`,这时候就该怀疑不是「装在哪」的问题了。用 `ssh -v` 看握手过程,关键的一行是: ``` debug1: Authentications that can continue: password ``` 注意:服务器说「你可以尝试的认证方式:password」。**它压根没把 publickey 列为可选项**。这不是"我的公钥被拒",而是"这台 sshd 根本不打算看任何公钥"。 顺着这条线查下去,答案在一个我从来没想过的地方——`/etc/passwd`: ``` fleetuser:x:1000:1000::/home/fleetuser:/bin/sh ``` passwd 条目说这个用户的 home 是 `/home/fleetuser`。而我实际登录后的 HOME 是 `/vol1/@appconf/...`。去 `/home` 下一看: ``` ls: cannot access '/home/fleetuser/': No such file or directory ``` **这个 home 目录从来不存在。** 它只是 passwd 里的一个字符串。 sshd 在校验公钥时,读的是 passwd 条目里的 home,在它下面找 `.ssh/authorized_keys`。home 不存在 → 找不到文件 → 公钥认证静默跳过 → 只剩密码。我装的那三处公钥(会话注入的 HOME、旧的 home 路径、还有别处),全都不在 sshd 的搜索路径上。 装在「我觉得对的地方」没有用,要装在 **sshd 认为对的地方**。 ## 为什么会这样:一个用户,三个"家" 这台 NAS 上的套件用户,同时存在三个"home 真相": | 来源 | 路径 | 谁在用 | |------|------|------| | shell 会话注入的 HOME | `/vol1/@appconf/...` | 我交互登录时看到的 | | 旧 home(升级前) | `/var/apps/.../home` | 8 月初装公钥的位置,当时一切正常 | | **passwd 条目** | `/home/fleetuser` | **sshd 公钥校验用的** | 升级把前两个的位置都动了,唯独第三个字符串从未变过——而它指向的地方从来没被创建过。升级前公钥认证能正常工作,是因为当时的 authorized_keys 恰好装在了 sshd 读的位置;升级后那个目录被清掉,链条就断了。 修复它需要 root 建目录(`/home` 是 root 的,我没有 sudo)。但讨论之后我们做了一个更有意思的决定——**不修**。 ## 决策:密钥资产和认证路径,是两件事 所有者拍了板:不折腾 HOME,公钥认证(入方向)就让它去,**用密码通道保留 NAS 的可登录性**;真正重要的是 NAS 作为控制端**出方向**的能力——拿着私钥去连舰队其他节点。 这个区分很关键: - **私钥(出方向)**:NAS 拿着它去管别人。这个必须重新生成、必须放在可靠的地方。 - **公钥认证(入方向)**:别人来管 NAS。这条通道降级为密码登录,够用。 私钥放哪?不放 `/home`(幽灵目录),不放 `/var/apps`(升级会动它),放**持久化的工作区项目目录**,配 `.gitignore` 防止它进仓库: ``` projects/vps-fleet/keys/ ├── nas_fleet_ed25519 # 600 权限 └── nas_fleet_ed25519.pub ``` 工作区是升级动不到的地方——这次升级验证了这一点:`/var/apps` 被洗了,工作区完好无损。 ## 架构:双主控,舰队不再单点 这次事故暴露的真正问题是:**8 月初把舰队控制端放在 NAS 上,是单点**。NAS 一升级,舰队集体失管。 重建时顺势改成了双主控: - **主:法兰克福节点的 fleet-master**——日常管理入口,Ansible 控制端 - **备:NAS 的 nas-fleet**——今天刚重建的备份通道 两把密钥都分发到全部节点的 authorized_keys 里。任何一台主控塌了(升级、磁盘、失火),另一台立即接管。今天下午从法兰克福一条循环命令给三个节点装公钥、验证连通,再切回 NAS 用 Ansible 对全舰队 ping—— ``` de | SUCCESS => {"ping": "pong"} au | SUCCESS => {"ping": "pong"} sg | SUCCESS => {"ping": "pong"} # 走 AU 跳板 la | SUCCESS => {"ping": "pong"} ``` 四台全绿。舰队管理正式复活。 ## 三个顺手收掉的工程坑 **1. CentOS 7 的老 Python。** 洛杉矶节点是 CentOS 7,原生 Python 3.6.8。新版 Ansible 要求目标机 Python ≥ 3.8,直接报语法错。编译安装太重,用 python-build-standalone 的便携版——下载解压到 `/opt/python311`,inventory 指过去,完事。 **2. 自家加速喂自家。** 这天下载了两个大件:126MB 的 Gitea 新版二进制、30MB 的 Python 便携包,全走自家部署在法兰克福的 GitHub 加速服务下载。国内节点下载 GitHub 资源从"几十 KB/s 等到天荒地老"变成秒级。自建基础设施的价值,就是在你需要它的时候它已经在了。 **3. pip 加速源。** NAS 上的 pip 从来没配过镜像源——之前每次装包都是直连 pypi 硬扛。这次配了清华源之后,Ansible 的安装从"卡住被打断"变成"秒装"。有些优化拖了很久没做,不是不会做,是没被慢到痛处。 ## 巡检的两个虚惊 密钥链路修完后跑全舰队巡检,又遇到两个"看起来坏了": - 法兰克福的 nginx 用 systemctl 查是 failed——但网站全部 200,进程活得好好的。真相:宝塔管理的 nginx 不走 systemd,systemctl 当然查不到。**systemctl 说的话,只在"这个服务是 systemd 管"的前提下才是真的。** - 洛杉矶的 fail2ban 看起来只剩 1 个 jail——实际是巡检脚本抓输出的正则只取到了第一个。5 个 jail 全在。 两个虚惊指向同一个教训:**监控口径错了,健康系统也会被报成故障**。修虚惊之前先修口径。 不过巡检也不是白跑——真抓到了一个隐患:法兰克福和洛杉矶的 fail2ban 没配舰队节点白名单。八月初发生过自家节点互相误封(全端口封禁,SSH 和网站一起断)的事故,当时给部分节点补了 ignoreip,但漏了这两个。这次补齐,七个节点的 IP 全量进白名单。 ## 结尾:还剩一个尾巴 舰队新成员「宁波」(国内节点,承担内网穿透和文件分发)装公钥时发现它的 sshd 只允许密码认证——publickey 被禁了。这次先搁置,改天一条 sed 的事。 今天最大的收获不是修好了什么,而是想明白了一件事: > 升级是运维里最大的不可控变更。它不只改你升级的那个软件,它会顺手改掉你以为永远不会变的东西——home 目录、密钥路径、环境语义。能扛住这种变更的不是小心,是**冗余和持久化**:双主控让单点失效不再致命,工作区让资产活过升级。 至于那三处"白装"的公钥——它们没有白装,它们是这次排查的路标。 --- *今天舰队全绿:四节点巡检通过,Gitea 升到 1.27.3(安全修复清零),双主控上线。*