2024 年,公司新采购了一批服务器,我负责把它们初始化好交给开发。配公钥这种事我做过无数次,闭着眼都能操作:把开发的公钥往 ~/.ssh/authorized_keys 里一追加,收工。可这次,开发拿着自己的私钥怎么都连不进来,清一色的 Permission denied (publickey)。我反复核对——公钥内容没错、一个字符都不差,文件也确实在那个位置。更怪的是,用密码登录这台机器,畅通无阻。公钥明明白白地躺在 authorized_keys 里,服务端却像没看见它一样,死活不认。我憋着这口气把这台机器修好,又顺手发现了另一个怪事:别人 SSH 登录这台机,敲完密码要干等将近 30 秒,才慢吞吞地蹦出命令行。这两件事逼着我把 SSH 的认证流程、文件权限、服务端日志这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一批新初始化的服务器
事故现象:
- 公钥已写进 ~/.ssh/authorized_keys,内容核对无误
- 用公钥登录:清一色 Permission denied (publickey)
- 用密码登录:完全正常
- 另一个怪事:登录后要干等近 30 秒才出命令行
现场排查:
# 1. ★ 客户端带 -v,看认证到底卡在哪一步
$ ssh -v -i ~/.ssh/id_ed25519 user@10.0.1.20
...
debug1: Offering public key: ~/.ssh/id_ed25519
debug1: Authentications that can continue: publickey,password
# ^^^ ★ 服务端把公钥【拒了】,只让继续试密码
# 2. ★ 服务端看日志 —— 真相都在这里
$ tail -f /var/log/secure
sshd[8123]: Authentication refused: bad ownership or
modes for directory /home/user/.ssh
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# ★ 真相:.ssh 目录的权限/属主不对!
# 3. 一看权限,果然
$ ls -ld /home/user /home/user/.ssh /home/user/.ssh/authorized_keys
drwxrwxr-x user user /home/user # ★ 组可写!
drwxrwxr-x user user /home/user/.ssh # ★ 777 味儿
-rw-rw-r-- user user /home/user/.ssh/authorized_keys # ★ 太开放
根因(后来想清楚的):
1. ★ sshd 有一条安全铁律:authorized_keys 以及它所在的
.ssh 目录、甚至【家目录】,权限都不能太开放。
2. 具体说:这几个,【组(group)和其他人(other)都不能有写权限】。
3. 一旦权限太松,sshd 会【直接拒绝用这个文件做公钥认证】,
而且默认【不告诉客户端为什么】—— 客户端只看到
Permission denied,真正的原因只写在服务端 /var/log/secure。
4. 这批新机器是从一个模板克隆的,家目录被设成了 775(组可写),
于是公钥认证集体失效。
5. ★ 登录慢那事是另一个根因:sshd 对每个连接做 DNS 反向解析,
而这批机器的 DNS 不通 -> 每次都干等到反解超时。
公钥认证失败,八成不是公钥的事,是【权限】的事。
修复 1:分清三种"连不上"——各指向什么
# === ★ SSH 连不上,先看报错是哪一种,方向完全不同 ===
# === 第 1 种:Connection refused(立刻被拒)===
$ ssh user@10.0.1.20
ssh: connect to host 10.0.1.20 port 22: Connection refused
# ★ 含义:包到了机器,但 22 端口【没有进程在监听】。
# -> sshd 服务没起来,或没监听在 22(改过端口)。
$ systemctl status sshd # 服务在不在跑
$ ss -lntp | grep ssh # sshd 到底听在哪个端口
# === 第 2 种:Connection timed out(干等到超时)===
$ ssh user@10.0.1.20
ssh: connect to host 10.0.1.20 port 22: Connection timed out
# ★ 含义:包【石沉大海】,没人理。
# -> 防火墙 DROP 了,或安全组没放行 22,或网络不通。
# (这一类属于防火墙排查的范畴,本文不展开)
# === 第 3 种:Permission denied(连上了,但认证没过)===
$ ssh user@10.0.1.20
user@10.0.1.20: Permission denied (publickey,password)
# ★ 含义:TCP 通了、sshd 也应答了,纯粹是【身份认证】没过。
# -> 这才是本文的主角。括号里列的是"还能试的认证方式"。
# === ★ Permission denied 后面的括号,信息量很大 ===
# (publickey) 服务端【只允许】公钥认证,你公钥没过
# (publickey,password) 公钥和密码都可以试,你都没过
# (gssapi-keyex,...) 还启用了别的认证方式
# ★ 如果括号里【没有 password】,说明服务端关了密码登录,
# 你只能靠公钥 —— 这时候公钥再不行,就真进不去了。
# === 排查前,先确认是不是连错了对象 ===
$ ssh -v user@10.0.1.20 2>&1 | grep -i 'connecting\|port'
# 确认 IP、端口、用户名都没敲错 —— 低级错误占的比例不低。
修复 2:公钥认证 Permission denied——权限是头号杀手
# === ★ 这次的根因:sshd 对公钥相关文件的权限,要求极严 ===
# === sshd 的安全铁律:这几样东西,组和其他人都不能可写 ===
# 1. 用户的【家目录】 ~ -> 不能 group/other 可写
# 2. ~/.ssh 目录 -> 推荐 700
# 3. ~/.ssh/authorized_keys -> 推荐 600
# ★ 道理:如果 .ssh 目录别人能写,别人就能往 authorized_keys
# 里塞自己的公钥,免密登进你的账号 —— 这是巨大的安全漏洞。
# 所以 sshd 宁可"拒绝服务",也不在不安全的权限下做公钥认证。
# === ★ 一键修正权限(公钥登不上,先无脑执行这几条)===
$ chmod 755 /home/user # 家目录:owner 全权,组/其他不可写
$ chmod 700 /home/user/.ssh # .ssh 目录:只有 owner 能进
$ chmod 600 /home/user/.ssh/authorized_keys # 公钥文件:只有 owner 读写
$ chown -R user:user /home/user/.ssh # ★ 属主也必须对
# 属主不对一样会被拒 —— 文件得真正属于那个登录用户。
# === 客户端这边:私钥权限太松,ssh 自己会拒绝用 ===
$ chmod 600 ~/.ssh/id_ed25519 # 私钥必须 600,否则 ssh 报错并忽略它
# === ★ 用 ssh -v 看客户端到底有没有"提交"这把钥匙 ===
$ ssh -v -i ~/.ssh/id_ed25519 user@10.0.1.20
debug1: Offering public key: ~/.ssh/id_ed25519 # 提交了这把
debug1: Authentications that can continue: publickey,password
# ★ 提交了却被拒 -> 钥匙本身没问题,是【服务端】不收
# -> 几乎必然是服务端文件权限 / authorized_keys 内容问题
# === 核对公钥到底有没有正确落到服务端 ===
$ cat ~/.ssh/id_ed25519.pub # 客户端的公钥
$ cat /home/user/.ssh/authorized_keys # 服务端收到的
# ★ 必须【整行完全一致】:ssh-ed25519 AAAA... 一个字符都不能差。
# 常见坑:复制粘贴时被换行截断、行尾混入空格、多了少了字符。
# === authorized_keys 的格式要点 ===
# - 一行一把公钥,不能换行折断
# - 行首可以加选项(如 from="1.2.3.4"),没有就直接是 ssh-xxx
# - 文件末尾留个换行
修复 3:服务端日志才是真相
# === ★ 客户端的 Permission denied 是"结果",原因在服务端 ===
# sshd 出于安全,默认【不把拒绝的具体原因告诉客户端】——
# 它只会写进服务端的认证日志。不看服务端日志,等于盲查。
# === SSH 认证日志在哪 ===
# CentOS / RHEL : /var/log/secure
# Ubuntu/Debian : /var/log/auth.log
$ tail -f /var/log/secure # 一边盯日志,一边让对方连
# === ★ 几条典型的服务端日志,直接点出病根 ===
# bad ownership or modes for directory /home/user/.ssh
# -> 权限/属主问题(就是这次)。回修复 2 改权限。
# Authentication refused: bad ownership or modes for file
# .../authorized_keys
# -> authorized_keys 文件本身权限太松。
# User user not allowed because not listed in AllowUsers
# -> sshd_config 里 AllowUsers 白名单没带上这个用户。
# Connection closed by authenticating user ... [preauth]
# -> 多半是公钥不匹配,或试了几次都没过。
# Invalid user xxx from ...
# -> 这个用户在服务端根本不存在。
# === ★ 用 ssh -vvv(客户端)对照 /var/log/secure(服务端)===
$ ssh -vvv -i ~/.ssh/id_ed25519 user@10.0.1.20
# 客户端 -vvv 看"我做了什么、走到哪一步",
# 服务端 secure 看"我为什么拒了你" —— 两边一对照,真相立现。
# === 实在查不出,临时让 sshd 在前台输出详细调试 ===
$ /usr/sbin/sshd -ddd -p 2222
# 用一个【临时端口 2222】跑一个调试版 sshd,-ddd 打满日志。
$ ssh -p 2222 user@10.0.1.20 # 连这个调试端口
# ★ 调试完 Ctrl+C 掉,别让这个临时 sshd 一直开着。
# === 看 sshd 实际生效的配置(别只看配置文件)===
$ sshd -T | grep -iE 'pubkey|password|permitroot|allowusers'
# sshd -T 打印【最终生效】的全部配置 ——
# 比翻 sshd_config 准,因为还有 Match 块、include 文件会覆盖。
修复 4:登录极慢——DNS 反解与 GSSAPI 超时
# === ★ 第二个怪事:敲完密码,要干等近 30 秒才出命令行 ===
# 这种"连得上、但慢得离谱"的卡顿,八成是某个【超时】在作祟。
# === 嫌疑 1:sshd 对每个连接做【DNS 反向解析】===
# sshd 默认会拿客户端 IP 去查它的主机名(反向 DNS)。
# ★ 如果这台机器的 DNS 不通 / 没配,这次反查就要【一直等到超时】,
# 每个人每次登录,都白白卡这一下。
$ vi /etc/ssh/sshd_config
UseDNS no # ★ 关掉反向解析
# 改完重启 sshd 生效。这是登录慢最常见的元凶。
# === 嫌疑 2:GSSAPI 认证在尝试连一个不存在的 Kerberos ===
$ vi /etc/ssh/sshd_config
GSSAPIAuthentication no # 没用 Kerberos 就关掉它
# 客户端侧也可以在 ~/.ssh/config 里 GSSAPIAuthentication no。
# === 改完配置,重载 sshd ===
$ sshd -t # ★ 先语法检查!配置写错会导致 sshd 起不来
$ systemctl reload sshd # 语法 OK 再重载
# === ★ 怎么判断到底是慢在"连接前"还是"认证后" ===
$ ssh -v user@10.0.1.20
# 盯着 -v 的输出,看它卡在哪一行不动:
# 卡在 "Connecting to ..." -> 网络 / 反向 DNS
# 卡在 "Authenticated to ..." 之后 -> 登录后的事(见下)
# 卡在认证过程中 -> GSSAPI 等认证方式超时
# === 嫌疑 3:认证过了,卡在【登录后】的脚本 / 服务 ===
# 如果是"认证成功之后"才卡,那跟 sshd 认证无关了,去查:
# - /etc/profile、~/.bashrc 里有没有执行慢命令(连远程接口等)
# - pam_systemd / systemd-logind:登录会创建 session,
# logind 异常会让登录卡顿
# - motd 动态脚本(/etc/update-motd.d/)跑了慢命令
$ systemctl status systemd-logind # logind 是否正常
# === 客户端侧也能加速:复用连接 ===
# ~/.ssh/config 里开 ControlMaster,同一台机第二个连接秒进:
# Host 10.0.1.20
# ControlMaster auto
# ControlPath ~/.ssh/cm-%r@%h:%p
# ControlPersist 600
修复 5:known_hosts 与 host key 警告
# === ★ 另一类常见 SSH 报错:host key 验证失败 ===
$ ssh user@10.0.1.20
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Offending key in /home/me/.ssh/known_hosts:42
# === 这个机制是干什么的 ===
# 你第一次连某台机,ssh 把它的【主机公钥指纹】记进 known_hosts。
# 以后每次连,都拿现在的指纹和记下的比对:
# 一致 -> 安心连;
# 不一致 -> ★ 报上面这个大警告 —— 防的是"中间人攻击"。
# === ★ 指纹变了,先冷静想:是正常变更,还是真有问题 ===
# 正常会变的情况:
# - 这台机【重装了系统】(host key 重新生成了)
# - IP 被回收后【分配给了另一台机器】
# - 端口转发 / 跳板机指向变了
# 危险的情况:
# - 你啥都没动,指纹突然变了 -> 警惕中间人,先别急着连!
# === 确认是正常变更后,删掉旧记录 ===
$ ssh-keygen -R 10.0.1.20 # ★ 精准删掉这一台的旧 known_hosts 记录
# 比手动编辑 known_hosts 第 42 行安全,不会误删别的。
$ ssh user@10.0.1.20 # 再连,会提示重新记录新指纹
# === 第一次连接就提示 "authenticity can't be established" ===
# 这是正常的 —— 你从没连过它,ssh 没它的指纹。
# ★ 严谨做法:核对它给出的指纹,和服务端 ssh-keygen -lf
# /etc/ssh/ssh_host_ed25519_key.pub 算出来的是否一致,再确认。
# === ★ 一个反面教材:别图省事全局关掉 host key 检查 ===
# StrictHostKeyChecking no 这种设置会让中间人警告形同虚设。
# 自动化脚本里如果要用,也应只对【特定可信内网主机】放宽,
# 别全局一关了之。
修复 6:SSH 排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 先分清三种连不上,方向完全不同 ===
# refused -> sshd 没起 / 端口不对
# timeout -> 防火墙 / 安全组 / 网络
# Permission denied -> 连上了,认证没过(本文重点)
# === 2. ★ 公钥认证失败,头号嫌疑是【权限】不是公钥 ===
$ chmod 755 ~ ; chmod 700 ~/.ssh ; chmod 600 ~/.ssh/authorized_keys
$ chown -R 用户:用户 ~/.ssh
# 家目录/.ssh/authorized_keys,组和其他人都不能可写。
# === 3. ★ 客户端 ssh -vvv,服务端 /var/log/secure,两边对照 ===
# 客户端只看到 Permission denied,真正原因在服务端日志里。
# === 4. 看生效配置用 sshd -T,别只翻 sshd_config ===
$ sshd -T | grep -i pubkey
# Match 块和 include 会覆盖,-T 才是最终生效的。
# === 5. 改完 sshd 配置,先 sshd -t 语法检查再 reload ===
$ sshd -t && systemctl reload sshd
# ★ 配置写错直接 restart,可能把自己锁在门外。
# === 6. 登录慢,先关 UseDNS 和 GSSAPIAuthentication ===
# 这两个超时是登录慢最常见的元凶。
# === 7. host key 警告先判断"是不是该变",别无脑删 ===
$ ssh-keygen -R 主机 # 确认是正常变更再删旧记录
# === 8. 排查 SSH 的命令链 ===
$ ssh -vvv user@host # ① 客户端:卡在哪一步
$ systemctl status sshd # ② 服务端:sshd 活着没
$ tail -f /var/log/secure # ③ 服务端:为什么拒我
$ ls -ld ~ ~/.ssh ~/.ssh/authorized_keys # ④ 权限对不对
$ sshd -T | grep -i pubkey # ⑤ 生效配置允许公钥吗
# 按这个顺序,SSH 连接问题基本能定位。
命令速查
需求 命令
=============================================================
客户端看认证全过程 ssh -vvv user@host
服务端看认证日志(CentOS) tail -f /var/log/secure
服务端看认证日志(Ubuntu) tail -f /var/log/auth.log
修家目录权限 chmod 755 ~
修 .ssh 目录权限 chmod 700 ~/.ssh
修公钥文件权限 chmod 600 ~/.ssh/authorized_keys
看 sshd 最终生效配置 sshd -T
sshd 配置语法检查 sshd -t
重载 sshd 配置 systemctl reload sshd
删某主机的旧 known_hosts ssh-keygen -R 主机
口诀:Permission denied 先查权限不是查公钥
客户端 -vvv 配服务端 secure 日志 -> 真相在服务端
避坑清单
- 三种连不上方向不同:refused 查 sshd,timeout 查防火墙,denied 查认证
- 公钥认证失败头号嫌疑是权限,家目录/.ssh/authorized_keys 组和其他人不能可写
- .ssh 目录推荐 700,authorized_keys 推荐 600,属主也必须是登录用户
- 客户端只看到 Permission denied,真正原因写在服务端 /var/log/secure
- ssh -vvv 配合服务端认证日志对照,是定位认证问题的标准打法
- 看 sshd 配置用 sshd -T 看最终生效的,别只翻 sshd_config
- 改完 sshd 配置先 sshd -t 语法检查再 reload,写错 restart 会把自己锁外面
- 登录慢先关 UseDNS 反向解析和 GSSAPIAuthentication,这俩超时最常见
- private key 私钥权限太松 ssh 会拒绝使用,客户端私钥要 chmod 600
- host key 变更警告先判断是不是该变,确认后用 ssh-keygen -R 精准删旧记录
总结
这次"公钥明明在、服务端却不认"的事故,纠正了我一个关于 SSH 公钥认证的、想当然的认知。在这次之前,我心里的公钥认证模型简单到只有一个环节:服务端的 authorized_keys 文件里,只要躺着一行和我私钥配对的公钥,认证就该通过——这件事的成败,在我看来,完全取决于"那一行公钥的内容,对不对得上"。所以当我反复核对、确认公钥内容一个字符都不差,服务端却依然铁青着脸甩回 Permission denied (publickey) 时,我陷入了纯粹的困惑:钥匙明明配得上锁,锁为什么就是不开?复盘到根上,我才真正理解,SSH 的公钥认证,从来不是"内容匹配"这一道关,它在比对公钥内容【之前】,还有一道我从来没意识到的、纯粹关于"安全"的关卡——文件权限审查。sshd 这个守门人,在它愿意翻开 authorized_keys 去读里面的公钥之前,会先把目光投向这个文件、以及它所在的 .ssh 目录、甚至再往上一层的用户【家目录】,审视它们的权限:只要这三者中的任何一个,对"组"或"其他人"开放了写权限,sshd 就会当场判定"这个环境不安全",然后【拒绝】使用这个 authorized_keys 做认证——它根本不会走到"读取公钥内容、和你的私钥比对"这一步。它的逻辑其实极其有道理:如果 .ssh 目录是别人可写的,那任何一个别有用心的人,都可以悄悄往 authorized_keys 里追加一行自己的公钥,从此免密登进你的账号——这是一个致命的安全漏洞。所以 sshd 的态度是:与其在一个不安全的权限环境下放行,它宁可"罢工",宁可把合法的你也一并挡在门外。我那批新服务器的真实处境正是如此:它们是从一个模板克隆出来的,家目录的权限被设成了 775——多出来的那个"组可写",就是压垮公钥认证的稻草。而这件事最折磨人的地方在于,sshd 出于安全考虑,默认【不会把这个真实原因告诉客户端】:它怕把"你的某某文件权限不对"这种细节透露给一个还没通过认证的人,会泄露信息、给攻击者递线索。于是真正的原因——那句清清楚楚的 bad ownership or modes for directory——被它默默地、只写进了服务端的 /var/log/secure 日志里。这就解释了我当时为什么会卡那么久:我一直死死盯着客户端那句语焉不详的 Permission denied 反复琢磨公钥内容,而真正的答案,从一开始就安安静静地躺在服务端的日志里,等着我去看一眼。这件事让我立下了排查 SSH 认证问题的两条铁律:第一,公钥认证失败,头号嫌疑永远是【权限】,而不是公钥内容本身,先无脑把家目录、.ssh、authorized_keys 的权限和属主修对再说;第二,客户端看到的 Permission denied 永远只是一个"结果",真正的"原因"在服务端——排查 SSH 认证,必须养成客户端开 ssh -vvv、服务端同时 tail 着 /var/log/secure,两边对照着看的习惯。这次事故的另一半,那个"登录要干等 30 秒"的怪事,则给我补了另一课:一个"连得上、却慢得离谱"的卡顿,几乎总是某个看不见的【超时】在背后作祟——这次是 sshd 默认会对每个连进来的 IP 做一次 DNS 反向解析,而这批机器的 DNS 根本不通,于是每一次登录,都白白卡在这次注定失败的反查上,直到它自己超时。关掉 UseDNS,30 秒的等待瞬间归零。这次从一个"钥匙配得上、锁却不开"的困惑出发,我最大的收获,是把脑子里那个只有"内容匹配"一个环节的公钥认证模型,换成了一个更完整的、分层的模型:SSH 的公钥认证,是先过"权限安全审查"这道关,再过"公钥内容比对"那道关——一把内容上完全正确的钥匙,会仅仅因为它被放在了一个 sshd 认为"不安全"的抽屉里,就被这位一丝不苟的守门人,客客气气却毫不通融地,挡在门外。
—— 别看了 · 2026