2024 年我写了个自动化部署脚本,需要从跳板机免密 ssh 到十几台目标机上执行命令。我按教程一步步来:ssh-keygen 生成密钥,把公钥拷到目标机的 authorized_keys 里,满心以为大功告成。结果一执行,ssh 还是弹出来一行 root@host's password:,跟我要密码。我换了密钥、重拷了公钥、重启了 sshd,折腾了一下午,那个密码提示就是阴魂不散。后来才查明白:我的公钥没有任何问题,authorized_keys 的内容也完全正确——真正卡住我的,是目标机上那个用户家目录的权限太松了,sshd 出于安全考虑,默默地拒绝了用这份公钥,然后悄无声息地退回去问我要密码。这件事逼着我把 SSH 公钥认证这套机制彻底理清了。本文复盘这次实战。
问题背景
环境:跳板机 ssh 到目标机,想配免密(公钥认证)
事故现象:
- ssh-keygen 生成了密钥,公钥也拷进了目标机 authorized_keys
- ssh 过去,依然弹出 password 提示
- 换密钥、重拷、重启 sshd 都没用
现场排查:
# 1. 用 -v 看 ssh 客户端到底在干什么
$ ssh -v root@目标机
...
debug1: Offering public key: /root/.ssh/id_ed25519
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: password
root@目标机's password:
# ★ 客户端确实把公钥递过去了(Offering public key),
# 但服务端没接受,直接 Next 到了 password ——
# 问题在【服务端为什么不认这把公钥】
# 2. 上目标机看 sshd 的日志
$ tail -20 /var/log/secure
sshd[2233]: Authentication refused: bad ownership or modes for
directory /root
# ★ 真相大白:"bad ownership or modes for directory /root"
# —— sshd 嫌 /root 这个目录的【权限模式不对】
# 3. 看看 /root 和 .ssh 的权限
$ ls -ld /root /root/.ssh /root/.ssh/authorized_keys
drwxrwxr-x root root /root # ★ 770,组可写!
drwxr-xr-x root root /root/.ssh # 755,也偏松
-rw-r--r-- root root /root/.ssh/authorized_keys # 644,也偏松
根因(后来想清楚的):
sshd 有一个默认开启的安全开关叫 StrictModes。
开着它时,sshd 在用公钥认证【前】,会先检查:
- 用户的【家目录】
- 家目录下的 .ssh 目录
- .ssh/authorized_keys 文件
这几样东西的【属主】和【权限】是不是足够"私密"。
只要它们能被【属主以外的人】写,sshd 就认为
"这把公钥可能被别人篡改过,不可信",于是
【拒绝使用公钥认证】,然后静静退回去用密码。
我的公钥没错,是家目录 770(组可写)触发了 StrictModes。
sshd 不会大声报错,它只是"默默不认",最具迷惑性。
修复 1:SSH 公钥认证的原理
# === 两种认证方式:密码 vs 公钥 ===
# 密码认证:每次连接都输密码 —— 不能自动化,也怕被暴力破解。
# 公钥认证:基于一对密钥,免密、且安全得多 —— 自动化部署必须用它。
# === 一对密钥:私钥 + 公钥 ===
# ssh-keygen 会生成两个文件:
# id_ed25519 私钥 —— ★ 留在【客户端】,绝不外传
# id_ed25519.pub 公钥 —— 拷到【服务端】,可以公开
# 私钥和公钥在数学上配对:公钥能验证"持有对应私钥的人"。
# === 认证流程(简化版)===
# 1. 客户端发起连接,表示"我想用公钥认证,这是我的公钥"
# 2. 服务端在该用户的 ~/.ssh/authorized_keys 里找这把公钥
# —— 找不到:不认,退回密码
# —— 找到了:进入第 3 步
# 3. 服务端用这把公钥出一道"只有对应私钥才能解的题"
# 4. 客户端用本地【私钥】解题,把答案传回
# 5. 服务端验证答案正确 -> 确认你持有私钥 -> 放行,免密登录
# ★ 全程私钥【从不离开客户端】,这是它安全的根本。
# === 服务端的关键文件:authorized_keys ===
# ~/.ssh/authorized_keys 里每一行,就是一把【被授权登录的公钥】。
$ cat ~/.ssh/authorized_keys
ssh-ed25519 AAAAC3Nz...xyz deploy@jumphost
# 想让某人/某机免密登进来,就把对方的【公钥】加一行进这个文件。
# === ★ 关键认知:authorized_keys 内容对,不代表就能用 ===
# sshd 在【读这个文件之前】,会先查它和它所在目录的权限 ——
# 权限不过关,文件内容再正确,sshd 也当它不存在。
# 这次事故的全部症结,就在这句话里。
修复 2:ssh-keygen 生成密钥、分发公钥
# === 第一步:在【客户端】生成密钥对 ===
$ ssh-keygen -t ed25519 -C "deploy@jumphost"
# -t ed25519 密钥类型,ed25519 是现在的推荐(短、快、安全)
# 老一点的用 -t rsa -b 4096
# -C 注释,通常写"谁@哪台机",方便日后辨认
# 一路回车:
# 保存路径 默认 ~/.ssh/id_ed25519(回车即可)
# passphrase 私钥口令 —— 自动化场景常留空,人用建议设
# 生成后会有两个文件:
$ ls -l ~/.ssh/
-rw------- 1 user user id_ed25519 # 私钥,权限自动是 600
-rw-r--r-- 1 user user id_ed25519.pub # 公钥
# === 第二步:把公钥分发到【服务端】===
# 方式 A:ssh-copy-id —— 最省事,它会自动处理目录和权限
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub root@目标机
# 它会让你输【一次】密码,然后把公钥正确地追加进目标机的
# authorized_keys,并把 .ssh 目录、文件的权限都设对 ——
# ★ 用 ssh-copy-id 能避开大半的权限坑,优先用它。
# 方式 B:手动追加(没有 ssh-copy-id 时)
$ cat ~/.ssh/id_ed25519.pub | ssh root@目标机 \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# ★ 手动方式一定要记得 chmod —— 这正是漏掉就出事的地方。
# === 第三步:验证免密生效 ===
$ ssh root@目标机
# 不再问密码,直接进去 = 成功。
# === 指定用哪把私钥连接 ===
$ ssh -i ~/.ssh/id_ed25519 root@目标机
# 一台机器上有多把私钥时,用 -i 明确指定。
修复 3:权限陷阱——这次的根因
# === ★ sshd 的 StrictModes:公钥认证的隐形门槛 ===
# /etc/ssh/sshd_config 里有个开关,默认就是开的:
# StrictModes yes
# 开着时,sshd 用公钥认证【之前】,会强制检查服务端这几样
# 东西的属主和权限,任何一样"太开放",就拒绝公钥认证。
# === 服务端必须满足的权限(三件套)===
# 1. 用户【家目录】本身
# —— 不能被 group 或 other 写。755 可以,775/770/777 不行。
$ chmod 755 /root # 或 700,关键是去掉 group/other 的 w
# 2. ~/.ssh 目录
$ chmod 700 ~/.ssh # 只有属主能读写进入
# 3. ~/.ssh/authorized_keys 文件
$ chmod 600 ~/.ssh/authorized_keys # 只有属主能读写
# 4. 属主也必须对 —— 这几样都得【属于那个登录用户本人】
$ chown -R root:root /root/.ssh
# === 一条命令把服务端权限全部设对 ===
$ chmod 755 ~ && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
$ chown -R $(whoami) ~/.ssh
# === ★ 为什么 sshd 要这么"龟毛" ===
# authorized_keys 决定了"谁能免密登进这个账号"。
# 如果这个文件、或它所在的目录、或家目录,能被【别人】写,
# 那别人就能往 authorized_keys 里偷偷加一把自己的公钥 ——
# 等于给自己开了一个后门。sshd 宁可拒绝公钥认证,也不
# 拿一份"可能被人动过手脚"的 authorized_keys 来放行。
# === 客户端的私钥权限也有要求 ===
$ chmod 600 ~/.ssh/id_ed25519
# 私钥若是 644(别人可读),ssh 客户端会直接报:
# "Permissions 0644 for 'id_ed25519' are too open"
# 并【拒绝使用这把私钥】。
# === 排查权限问题,服务端日志是铁证 ===
$ tail -f /var/log/secure # CentOS
$ tail -f /var/log/auth.log # Debian/Ubuntu
# "bad ownership or modes for" 这行,直接点名是哪个目录/文件的锅。
修复 4:用 ssh -v 调试认证过程
# === 免密不生效,ssh -v 是第一诊断工具 ===
$ ssh -v root@目标机 # -v 输出调试信息
$ ssh -vv root@目标机 # 更详细
$ ssh -vvv root@目标机 # 最详细
# === -v 输出里要重点看的几行 ===
# 1. 客户端用了哪把私钥
debug1: Offering public key: /root/.ssh/id_ed25519
# 没有这行 -> 客户端根本没拿你以为的那把钥匙(看修复 5 的 config)
# 2. 服务端接受了哪些认证方式
debug1: Authentications that can continue: publickey,password
# 3. 公钥认证的结果
debug1: Server accepts key: ... # ★ 这行 = 公钥被接受了
# —— 没有这行,却直接跳到:
debug1: Next authentication method: password
# = 公钥认证【失败】,退回了密码。问题在服务端(多半是权限)。
# === 服务端同步看日志,两边对照 ===
# 在目标机上:
$ tail -f /var/log/secure
# 常见的几种服务端报错:
# "Authentication refused: bad ownership or modes for directory /root"
# -> 家目录/.ssh/authorized_keys 权限太松(修复 3)
# "Could not open authorized keys '/root/.ssh/authorized_keys'"
# -> 文件不存在,或路径不对
# "key_read: ... " / 公钥那行格式坏了
# -> authorized_keys 里的公钥被换行/截断了
# === 临时拉高 sshd 自己的日志级别(查疑难)===
$ vim /etc/ssh/sshd_config
LogLevel DEBUG1 # 默认 INFO,调成 DEBUG1
$ systemctl restart sshd
# 之后 /var/log/secure 里会有 sshd 极详细的认证过程。
# === 验证 sshd 配置文件本身没写错 ===
$ sshd -t # 测试配置语法,没输出就是 OK
# ★ 改完 sshd_config,先 sshd -t 验证,再 restart ——
# 配置写错又重启,可能把自己彻底关在门外。
修复 5:~/.ssh/config 与 ssh-agent
# === 问题:ssh 命令越敲越长 ===
$ ssh -i ~/.ssh/deploy_key -p 2222 deploy@10.0.0.5
# 十几台机器,每台参数都不同,记不住也敲不动。
# === 解法:~/.ssh/config —— 给每台机器起个别名 ===
$ vim ~/.ssh/config
Host web1 # 别名,随便起
HostName 10.0.0.5 # 真实 IP/域名
User deploy # 登录用户
Port 2222 # 端口
IdentityFile ~/.ssh/deploy_key # 用哪把私钥
Host db* # ★ 支持通配:db1 db2 都匹配
User root
IdentityFile ~/.ssh/db_key
Host * # 对所有主机生效的通用设置
ServerAliveInterval 60 # 每 60s 发个心跳,防连接被掐
ServerAliveCountMax 3
# 配好之后:
$ ssh web1 # 等价于上面那一长串
$ scp file.tar web1:/tmp/ # scp 也认这个别名
# === ★ config 文件本身也要管权限 ===
$ chmod 600 ~/.ssh/config
# === ssh-agent:私钥设了口令时,免得反复输 ===
# 如果私钥设了 passphrase,每次连接都要输口令,很烦。
$ eval $(ssh-agent) # 启动 agent
$ ssh-add ~/.ssh/id_ed25519 # 把私钥加进 agent,输【一次】口令
$ ssh-add -l # 看 agent 里现在有哪些私钥
# 之后这个终端会话里,连接都不再问口令了。
# === agent forwarding:从 A 经 B 跳到 C ===
$ ssh -A web1 # -A 开启 agent 转发
# 在 web1 上再 ssh 别的机器时,能用本地 agent 里的私钥,
# 不用把私钥拷到 web1 上。★ 但 -A 有安全风险,只对可信机器用。
# === 排查"用错了私钥" ===
$ ssh -v web1 2>&1 | grep -i 'offering\|identity'
# 看 ssh 实际在用哪把钥匙 —— 和你 config 里写的对不对得上。
修复 6:SSH 安全加固纪律
# === 配好免密只是第一步,SSH 是攻击重灾区,要加固 ===
$ vim /etc/ssh/sshd_config
# === 1. ★ 公钥认证调通后,关掉密码登录 ===
PasswordAuthentication no
# 关掉密码登录,暴力破解就彻底无从下手 ——
# 这是 SSH 加固里收益最大的一条。
# ★ 前提:务必先确认公钥能登进来,否则会把自己锁在外面。
# === 2. 禁止 root 直接登录 ===
PermitRootLogin no
# 改用普通用户登录,再 sudo 提权。
# root 是人人皆知的账号名,禁掉它能挡掉大量盲扫。
# === 3. 改掉默认 22 端口(减少噪音)===
Port 22000
# 不能真正防黑客,但能让自动化扫描的噪音骤减。
# ★ 改端口后,记得放行防火墙、更新 ~/.ssh/config 的 Port。
# === 4. 限制能登录的用户 ===
AllowUsers deploy ops # 只允许这两个用户登录
# 或用 AllowGroups 按组放行。
# === 5. 其他几个推荐项 ===
MaxAuthTries 3 # 单次连接最多试 3 次认证
LoginGraceTime 30 # 30 秒内没登录成功就断开
StrictModes yes # ★ 保持开启 —— 它正是修复 3 的主角
# === 改完的标准动作:先验证,再重启 ===
$ sshd -t # 测试配置语法
$ systemctl restart sshd
# ★ 强烈建议:重启后【保留当前这个 ssh 会话别关】,
# 另开一个新窗口测试能不能登进来。能,再关旧会话。
# 万一配置有问题,旧会话还在,你还能改回来。
# === 6. 配合 fail2ban 自动封禁暴力破解 IP ===
$ yum install fail2ban -y
# fail2ban 监控 /var/log/secure,某 IP 多次登录失败就自动封它。
# === 7. 定期巡检 ===
$ lastb | head # 看最近的【失败】登录尝试
$ last | head # 看最近的成功登录
$ cat ~/.ssh/authorized_keys # ★ 定期核对:有没有多出不明的公钥
命令速查
需求 命令
=============================================================
生成密钥对 ssh-keygen -t ed25519 -C 注释
分发公钥到服务端 ssh-copy-id -i 公钥 user@host
调试 ssh 认证过程 ssh -v / -vv / -vvv user@host
看服务端认证日志 tail -f /var/log/secure
设服务端 .ssh 权限 chmod 700 ~/.ssh
设 authorized_keys 权限 chmod 600 ~/.ssh/authorized_keys
设家目录权限 chmod 755 ~
测试 sshd 配置语法 sshd -t
把私钥加进 agent ssh-add ~/.ssh/id_ed25519
看最近失败登录 lastb | head
口诀:免密失败先 ssh -v 看公钥递没递出去
-> 再上服务端看 /var/log/secure -> bad modes 就是权限太松
-> 家目录755 / .ssh 700 / authorized_keys 600
避坑清单
- 公钥内容正确不代表能免密,sshd 会先检查文件和目录权限
- StrictModes 默认开启,家目录能被属主以外的人写就拒绝公钥认证
- 服务端三件套:家目录 755、~/.ssh 700、authorized_keys 600
- 这几样东西还必须属于登录用户本人,属主不对一样被拒
- 客户端私钥权限必须 600,太开放 ssh 会直接拒绝使用它
- 免密失败先 ssh -v 看有没有 Offering public key,再看服务端日志
- 服务端 /var/log/secure 的 bad ownership or modes 直接点名权限问题
- 优先用 ssh-copy-id 分发公钥,它会自动把权限设对
- 改完 sshd_config 先 sshd -t 验证再 restart,并保留旧会话防锁死
- 公钥认证调通后再关密码登录、禁 root 登录,这是收益最大的加固
总结
这次"配了免密却还要密码"的事故,纠正了我对 SSH 公钥认证一个很想当然的认知:我一直以为,公钥认证这件事,只要"公钥本身没错"加上"公钥确实拷进了 authorized_keys 文件"这两个条件成立,就一定能成。我折腾了一整个下午,换密钥、重拷公钥、重启 sshd,反复确认的全都是这两件事,可问题始终没解决——因为真正卡住我的,是我那一下午压根没往那儿想的第三个条件:权限。复盘到根上,我才真正理解了 SSH 公钥认证背后那条我一直忽略的安全逻辑。一个用户的 ~/.ssh/authorized_keys 文件,它的分量其实非常重——这个文件里每多一行公钥,就等于多授权了一个人可以免密登进这个账号。正因为它这么关键,sshd 在拿它来放行任何人之前,会先做一件极其谨慎的事:它会检查这个 authorized_keys 文件本身、检查它所在的 .ssh 目录、还要一直检查到用户的整个家目录,看这三样东西是不是足够"私密"。这个检查由一个默认就开启的开关 StrictModes 来掌管。sshd 的判断标准很简单也很严厉:这三样东西里,只要有任何一样,能够被它的属主之外的人写入,sshd 就会认定——这份 authorized_keys 是"不可信"的。它的推理是这样的:如果别人能写我的家目录,那别人就能在我的 .ssh 目录里动手脚;如果别人能写我的 .ssh 目录,那别人就能替换掉我的 authorized_keys;如果别人能直接写 authorized_keys 这个文件,那别人就能往里面偷偷塞一把属于他自己的公钥——无论是哪一种,结果都是有人能不声不响地给自己开一个登进我账号的后门。所以 sshd 的选择是:与其拿一份"可能已经被别人篡改过"的 authorized_keys 去冒险放行,它宁可干脆当这个文件不存在,直接拒绝走公钥认证这条路。而我这次踩的坑,正在于此:我那台目标机上,root 用户的家目录 /root 的权限是 770——这个 770 里的那个"组可写",就成了 sshd 眼里那个致命的"别人能写"。于是 sshd 默默地、不动声色地拒绝了我那把其实完全正确的公钥。最有迷惑性的地方就在这个"默默"上:sshd 拒绝公钥认证之后,并不会在我的 ssh 客户端这边弹出一行刺眼的"权限错误,公钥被拒",它只是平静地、悄无声息地退回到下一种认证方式,也就是问我要密码。我看到的,永远只是那行平平无奇的 password 提示,而真正的原因——那行清清楚楚写着 bad ownership or modes for directory /root 的记录——它从来不在我的客户端,它一直安安静静地躺在目标机的 /var/log/secure 日志里,等着我想起来去看它。这次事故之后,我把排查免密问题的思路彻底重建了。第一,免密一旦不生效,我不再盲目地换密钥重拷公钥,我做的第一件事是 ssh -v,看我的客户端到底有没有把公钥递出去;第二,只要客户端确实递了公钥、服务端却没接受,我立刻就转到服务端,去翻 /var/log/secure——这个日志几乎总会用一句大白话,直接点名到底是哪个文件、哪个目录的权限或属主出了问题;第三,我把服务端公钥认证的"权限三件套"刻进了肌肉记忆——用户的家目录不能被别人写(755 或 700)、.ssh 目录必须是 700、authorized_keys 文件必须是 600,而且这三样都必须实实在在地属于那个要登录的用户本人。还有一个更省心的习惯,是分发公钥时尽量用 ssh-copy-id 而不是手动 cat 追加——因为 ssh-copy-id 会替我把目录和文件的权限自动设置妥当,而大半的免密事故,恰恰就出在手动追加时漏掉了那几条 chmod。这次从一个甩不掉的密码提示出发,我最大的收获,是终于明白了:在 SSH 公钥认证这件事上,"内容正确"和"能够被信任"是两个完全独立的条件——公钥的内容写得再对,只要它栖身的那个目录不够"私密",sshd 就不会信任它;而安全,从来都是由整条链路上最松的那一环来决定的。
—— 别看了 · 2026