Permission denied (publickey) 是所有 SSH 报错里最常被撞上的那个,而它这句话对你一点忙都没帮上。它并不意味着“你的密钥错了”。它的意思是:**服务器拒绝了你提供的每一种认证方式,而公钥是它唯一愿意讨论的方式。**括号里是服务器仍然开放的方式清单——(publickey) 意味着密码认证已关闭,所以必须有一把密钥能用,而你的密钥一把都没成。
这个清单可能更长,而且值得一读。Permission denied (publickey,password) 意味着服务器也接受密码——所以如果你本打算输一个密码、却从没被提示过,那是你的客户端在密钥上放弃了,本该回退到密码;用 -o PubkeyAuthentication=no 强制弹出密码提示来测一下。Permission denied (publickey,keyboard-interactive) 是同样的意思,只是还摆着一种交互式方式(常常是一个 PAM 提示或双重验证)。括号里的内容就是服务器留开的那组门的完整清单;如果你指望的那扇不在里面,那服务器从一开始就不会接受它。
这张网撒得很宽:用户名错了会产生这个报错。客户端从没提供过的密钥、服务器从没收到过的密钥、一个 sshd 默默不肯将就的权限问题,以及一个你从没听说过的 SELinux 标签,也都会。解决办法是系统性的,而不是靠猜——下面先给出这套循环,再按它们实际上是答案的频率高低排列各个罪魁祸首。(在往 GitHub 或 GitLab 推代码?那是另一份清单——GitHub/GitLab SSH 密钥用不了。)
诊断循环:读懂客户端尝试了什么
用最高的详细级别跑一遍那条失败的连接(如果满屏的输出看起来像噪声,如何读懂 ssh -vvv 的输出(英文)会帮你解码其中要紧的几行):
ssh -vvv user@host
在输出里要找三样东西:
Authentications that can continue: publickey——服务器的方式清单。如果password不在其中,那就永远不会有人问你密码;必须有一把密钥成功。Offering public key: /home/you/.ssh/id_ed25519——客户端尝试过的每一把密钥。如果你想用的那把不在这个列表里,那问题出在客户端一侧(罪魁二)。如果它在——- 返回了什么。
Server accepts key后面跟着一次签名交换,意味着密钥匹配上了、而是后面某一步失败了(少见)。常见的景象是:刚提供完,紧接着就是下一把密钥或者Permission denied——服务器看了一眼这把密钥,耸了耸肩,这就指向服务器一侧(罪魁三到八)。
如果你还有别的办法进那台机器(服务商的网页控制台、另一个用户、另一位管理员),服务器的日志会直接点出真正的原因:
sudo journalctl -u sshd -f # systemd distros
sudo tail -f /var/log/auth.log # Debian/Ubuntu
sudo tail -f /var/log/secure # RHEL/Alma/Rocky
像 Authentication refused: bad ownership or modes for directory /home/deploy 这样的行,能当场结束整个排查。
罪魁一:用户错了
最常见的单一原因,在云镜像上尤其如此(它就是第一次登录 EC2 或 VPS(英文)撞上的第一堵墙),因为登录用户往往不是你猜的那个:Ubuntu 镜像上是 ubuntu,Amazon Linux 上是 ec2-user,Debian AMI 上是 admin,大多数裸 VPS 上是 root,Azure 默认是 azureuser。对的密钥配上错的用户名,产生的正是这个报错——服务器去检查 ~wronguser/.ssh/authorized_keys,什么都没找到,于是拒绝。
在动其他任何东西之前,先确认你的镜像文档里写的用户,并把它钉在 ~/.ssh/config 里,这样你就再也不用重敲它了(ssh_config 指南(英文)讲了这种写法):
Host web1
HostName 203.0.113.10
User ubuntu
罪魁二:你的密钥根本没被提供
如果 ssh -vvv 没有在某行 Offering public key 里显示你的密钥,那说明客户端根本不知道它的存在。显式地指向它:
ssh -i ~/.ssh/id_ed25519 user@host
如果这样能成,就用上面配置块里的 IdentityFile 把它固定下来。两个相关的坑:一个塞了很多密钥的代理,可能在轮到对的那把之前就触发了服务器的尝试次数上限(Too many authentication failures——用 IdentitiesOnly yes 解决),而一把被你取消了密码短语输入的密钥会被悄悄跳过。查一下代理里实际有些什么:
ssh-add -l
在 Windows 上,哪怕你刚刚加载过一把密钥,这个列表也可能看起来是空的——Git Bash、OpenSSH 服务和 WSL 各自跑着一个独立的代理,而 ssh-add -l 只会替你当前真正在用的那个 ssh 作答;Windows 上的 ssh-agent把这三者理清楚。
罪魁三:公钥不在服务器上
公钥那一半必须是 ~user/.ssh/authorized_keys 里的一行——针对那个用户,在那个用户的家目录里。把它放上去的可靠办法(拷贝时会用到密码认证或一把已有的密钥):
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
手动来做——比如通过服务商的控制台——要追加这一行,绝不要覆盖整个文件:
mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys
# paste the one line from id_ed25519.pub, Enter, Ctrl-D
几个经典的疏漏:密钥落进了 root 的文件,但你是以 deploy 登录的;那一行被编辑器折行了,现在成了两行;你粘贴的是私钥(公钥文件以 ssh-ed25519 AAAA… 开头——如果你看到的是 -----BEGIN,那就是错的那一半)。
罪魁四:权限与属主
sshd 的 StrictModes(默认开启)会拒绝认可 authorized_keys,只要通往它的路径上有属主之外的任何人可写。这就是那个经典的“我把密钥拷过去了,可它还是不行”。完整的修复,以目标用户身份运行:
chmod go-w ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R $USER:$USER ~/.ssh
家目录那一行很重要,却总被忘掉:一个对组可写的 /home/deploy,和一个人人可读的密钥文件一样,照样过不了这道检查。服务器日志里的 bad ownership or modes 那一行,会明确告诉你它反对的是哪个目录。
罪魁五:密钥格式或密码短语不对
OpenSSH 读不了 PuTTY 的 .ppk 文件——把 -i 指向这样一个文件,详细输出里会报 invalid format。从 PuTTYgen 里导出一把 OpenSSH 格式的密钥(Conversions → Export OpenSSH key),或者干脆生成一把全新的现代密钥并登记上去(几乎所有情况下都选 Ed25519(英文))。如果你在 Windows 上、还在 PuTTYgen 里打转,一款能导入 PuTTY 会话、直接使用 OpenSSH 密钥的 Windows SSH 客户端,能把这道转换步骤彻底省掉。要确认一个私钥文件可读、且密码短语就是你以为的那个,让它把自己的公钥那一半打印出来:
ssh-keygen -y -f ~/.ssh/id_ed25519
如果它打印出了一把密钥,那文件和密码短语都没问题——而且你可以把这段输出和 authorized_keys 里的那一行做对比,确认它们确实是同一把密钥。
罪魁六:服务器配置不允许
不太常见,但在你有服务器访问权时,检查的成本很低。问一下 sshd 它实际是用什么配置在跑:
sudo sshd -T | grep -Ei 'pubkeyauthentication|authorizedkeysfile'
PubkeyAuthentication 必须是 yes(默认如此,但加固脚本有时会把它翻掉),而 AuthorizedKeysFile 告诉你 sshd 去哪里找——如果有人把它指向了 /etc/ssh/authorized_keys/%u,那你精心放好的 ~/.ssh/authorized_keys 就被忽略了。对 /etc/ssh/sshd_config 做任何改动之后:
sudo systemctl reload sshd
罪魁七:SELinux 上下文(RHEL 系)
在 RHEL、Alma、Rocky、Fedora 这些系统上,一个由非常规方式创建的 ~/.ssh——从备份恢复、以 root 身份拷贝、在挂载的卷上创建——可能带着错误的 SELinux 标签,于是 sshd 对一个权限完美无缺的文件被拒绝读取。上面的一切都没问题,报错却依旧,而 ls -Z ~/.ssh 显示的是 ssh_home_t 之外的东西。一条命令就能修好这些标签:
restorecon -R -v ~/.ssh
如果你在一台 RHEL 系机器上已经没辙了,那就直接跑一下——标签本来就对的话,它也没有害处。
罪魁八:root 登录被禁用
现代默认值会把 PermitRootLogin 设成 prohibit-password(仅密钥)或 no(永不允许)。如果你在尝试 root@host、而密钥确确实实就在 /root/.ssh/authorized_keys 里,查一下:
sudo sshd -T | grep -i permitrootlogin
设成 no 时,任何凭据对 root 都永远不会管用——以非特权用户登录再 sudo,反正这也正是默认值在推着你走的那种安排。
快速解答
Permission denied (publickey) 是不是说我的密钥坏了?
几乎从来不是。它的意思是服务器没有接受你提供的任何一种方式——通常是用户错了或密钥错了,而不是密钥坏了。用 ssh-keygen -y -f ~/.ssh/id_ed25519 确认密钥可读、密码短语正确。
(publickey) 和 (publickey,password) 有什么区别?
括号里列的是服务器仍然允许的方式。(publickey) 是只接受密钥;(publickey,password) 意味着密码也能用,所以你本该被提示过——如果没有,那是你的客户端在把密钥都试完之后放弃了。
昨天还好好的,现在却失败了——变了什么?
一台被重建的服务器、一把被轮换的密钥,或者一个超时后丢掉密钥的代理。先跑 ssh-add -l 看看加载了什么,再确认那台机器是不是被重装了镜像(那也会抛出主机密钥验证失败(英文))。
让它别再发生
上面几乎每一个罪魁都是一条你不该每次都重新推导的、按主机而定的事实:用哪个用户、哪把密钥、哪个端口。把它们钉在 ~/.ssh/config 里,这一类报错就基本消失了——而对于剩下的那些报错,这本“解码环”讲了你还会碰到的另外九个。
同样这些事实,可以存在一个客户端里,而不是一个点文件里。Termalin 把用户、端口和密钥连同保存好的主机一起存起来,通过它的内置代理把你的密钥解锁一次(不会再因为取消输入密码短语而悄悄跳过一把密钥),在首次连接时验证服务器的身份,并按主机显示实时状态——这样一来,“它到底开着没?”和“我用的是对的密钥吗?”就不再是排查步骤了。它还能直接导入 PuTTY 会话(英文),这就把罪魁五永久退役了。
Termalin 是一款免费、跨平台的 SSH 客户端,把按主机而定的用户、端口和密钥都随主机一起保存,并在首次连接时验证服务器——立即下载,或了解它如何安全地处理密钥。