配了免密却还要密码:一次 Linux SSH 公钥认证排查复盘

公钥拷进了 authorized_keys,ssh 过去却仍弹密码。排查梳理:SSH 公钥认证原理、ssh-keygen 生成与 ssh-copy-id 分发、StrictModes 为何因家目录权限太松而拒绝公钥、用 ssh -v 调试认证过程、~/.ssh/config 与 ssh-agent,以及一套 SSH 安全加固纪律。

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

避坑清单

  1. 公钥内容正确不代表能免密,sshd 会先检查文件和目录权限
  2. StrictModes 默认开启,家目录能被属主以外的人写就拒绝公钥认证
  3. 服务端三件套:家目录 755、~/.ssh 700、authorized_keys 600
  4. 这几样东西还必须属于登录用户本人,属主不对一样被拒
  5. 客户端私钥权限必须 600,太开放 ssh 会直接拒绝使用它
  6. 免密失败先 ssh -v 看有没有 Offering public key,再看服务端日志
  7. 服务端 /var/log/secure 的 bad ownership or modes 直接点名权限问题
  8. 优先用 ssh-copy-id 分发公钥,它会自动把权限设对
  9. 改完 sshd_config 先 sshd -t 验证再 restart,并保留旧会话防锁死
  10. 公钥认证调通后再关密码登录、禁 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

三个月备份全部落空:一次 Linux crontab 定时任务排查复盘

2026-5-20 18:21:49

Linux教程

几百个僵尸塞满进程表:一次 Linux 进程信号排查复盘

2026-5-20 18:28:31

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索