ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSH 公钥认证失败排查:从 permission denied 到 authorized_keys 全解析

SSH 公钥认证失败排查:从 permission denied 到 authorized_keys 全解析 兄弟们如果你在终端里敲下ssh userserver回车后看到的是这么一行冷冰冰的提示ssh: permission denied (publickey)然后不管你是反复确认密码、重新生成密钥还是网上翻了几十个帖子服务器都像聋了一样不认你——恭喜你你来到了 SSH 排错路上最高频、也最让人抓狂的十字路口。这个报错翻译成大白话就是服务端拒绝了你基于公钥的认证请求你在对方系统里压根不算“可信人员”。最难受的是它不告诉你哪儿错了、缺了哪一步、认证在哪个环节断的一切都得自己从零摸排。这个错误几乎天天出现在 VSCode 远程开发、Git/GitLab 仓库推送、Jenkins 自动化部署、服务器批量登录这些场景里折腾过的人应该都懂那种痛苦明明密钥生成过了公钥也放进authorized_keys了权限也改了可它就是连不上。老手遇到可能几分钟定位新手往往要卡一下午。这篇文章我想把 SSH 公钥认证这条链路完整拆开从认证原理、服务端和客户端配置到 VSCode 远程连接、Git 认证、Jenkins 自动化、批量登录这些高频场景把排查思路和避坑经验都捋清楚。无论你是刚入门的小白还是被这个报错折磨过的运维/开发老哥按着这篇文章的思路走一遍大概率能在几分钟内自己定位问题。1. 先搞懂公钥认证的完整链路别瞎猜1.1 认证流程拆解客户端和服务端到底在做什么很多人一看到permission denied (publickey)就急着改配置却连 SSH 公钥认证到底是怎么运作的都没搞明白这样排查基本靠蒙。我习惯用一个类比来解释公钥认证就像访客登记制度。你的公钥id_ed25519.pub相当于工牌上那张照片~/.ssh/authorized_keys里的内容相当于门卫手里的花名册而你的私钥id_ed25519则像是工牌里的加密芯片。完整的认证流程大概是这样的客户端连上服务器的 22 端口后双方先商量一套双方都支持的加密算法和密钥交换方式然后服务端把自己支持的认证方式列表告诉客户端比如publickey、password、keyboard-interactive这些。如果客户端决定用公钥认证它会先拿出一把公钥递给服务端说“你看看花名册里有没有我”。服务端在对应的authorized_keys文件里查找这把公钥如果找到了并不会直接放行而是出一道只有私钥才能解开的“谜题”。客户端把谜题交给私钥去签名/解密再把结果送回服务端服务端用刚才那把公钥去验证签名是否正确。只有签名对上了你才算真正通过认证。到了这一步你就明白了permission denied (publickey)其实是在说“你用来证明身份的那套公钥/私钥组合服务端不认”。它可能发生在好几个环节——要么服务端根本没找到你的公钥要么找到了但签名验证对不上要么服务端压根就不允许公钥认证这种方式。错误信息不会告诉你具体是哪个环节所以你需要一套清晰的排查路径而不是像个无头苍蝇一样反复生成密钥、反复粘贴。1.2 排在前面的两件事确认认证方式和检查配置是否生效有一个特别容易被忽略的点报错里写着publickey不代表密码认证就一定被禁了。ssh: permission denied (publickey)指的是在当前这次连接中客户端尝试的所有认证方法里服务端只接受了publickey这一个而恰好公钥认证又失败了。如果服务端同时允许password和publickey客户端默认会先试公钥失败后一般会继续尝试密码让你输入密码登录。如果你看到的报错只有permission denied (publickey)而没有任何输入密码的提示通常说明服务端只开了公钥认证或者密码认证被禁用了。很多云服务器厂商的默认镜像正是这种配置就是为了安全考虑只允许密钥登录。第二件事检查配置是否真的生效。很多人辛辛苦苦改了/etc/ssh/sshd_config但忘了重启sshd服务或者改的时候手滑改到了客户端配置/etc/ssh/ssh_config这两个文件名字只差一个 d但一个是服务端配置一个是客户端配置搞错了等于白改。修改服务端配置后一定要执行重启或重载命令比如sudo systemctl restart sshd或sudo systemctl reload sshd。改了不重启等于跟服务器讲“下次再说”它记不住。2. 从零开始的系统排查路径2.1 第一步永远看日志日志比感觉可靠排查任何 SSH 问题我的第一反应永远是打开服务端日志而不是反复改客户端配置。日志会直接告诉你服务端在认证阶段到底拒绝了什么、为什么拒绝。Debian/Ubuntu 系统日志在/var/log/auth.logCentOS/RHEL 类系统在/var/log/secure。常用的几个命令# 查看最近的 sshd 日志 sudo grep sshd /var/log/auth.log | tail -n 50 # 实时跟踪日志边操作边观察 sudo tail -f /var/log/auth.log我碰到过一个特别典型的案例。有个同事信誓旦旦说“公钥肯定放进去了”结果日志第一行就是Authentication refused: bad ownership or modes for directory /home/user。这种问题在日志里一目了然但如果你不看日志光靠猜可能永远猜不到是目录权限的锅。还有几个常见日志关键词也要认识一下Failed publickey for user xxx from IP服务端确实收到了公钥认证请求但验证失败了通常是公钥没匹配上或私钥签名错了。no matching key exchange method/no matching cipher这不是认证问题而是算法协商失败多见于 OpenSSH 版本差距过大。Connection closed by authenticating user xxx客户端在认证过程中直接断开常见于~/.ssh/authorized_keys格式不对或权限有问题。看日志这一步能帮你排除至少一半的“灵异事件”。很多人绕开日志直接重装 SSH、重启机器结果问题照旧就是因为没看日志。2.2 服务端三大配置项逐一核对如果日志里没有明显线索那就去核对服务端配置。用到最多的几个配置项如下你可以用一条命令把实际生效的配置拉出来而不是只看文件里的注释sudo sshd -T | grep -E pubkeyauthentication|authorizedkeysfile|permitrootlogin|passwordauthentication这条命令直接输出服务端正在使用的配置值比打开sshd_config猜哪一行生效靠谱得多。重点关注三项PubkeyAuthentication必须为yes否则整个公钥认证都被直接禁止AuthorizedKeysFile指定了公钥存放路径默认是.ssh/authorized_keys如果被改过你把公钥放到默认路径就永远对不上PermitRootLogin的值决定了 root 用户能不能用公钥登录很多发行版默认是prohibit-password意思是 root 可以密钥登录但禁止密码登录如果你连的是 root 用户这个配置不对就直接失败。顺带提一句Ubuntu 新装系统默认禁用了密码登录和 root 密码登录如果你手头只有密码没有密钥会很痛苦。这种情况下若确认服务端允许 root 密钥登录需要先通过其他方式比如云控制台 VNC进入系统把公钥放进 root 的authorized_keys再重试。2.3 检查客户端手里的牌公钥和私钥对不对服务端看完了再看客户端这边。先确认本地有没有生成过密钥以及私钥是否能被客户端找到ls -la ~/.ssh/ ssh-add -lssh-add -l是查看当前 ssh-agent 里加载了哪些私钥。如果你用的是 2048 位以上的新版密钥OpenSSH 默认会在加载时检查私钥文件权限太宽松直接拒绝加载。另外注意客户端默认会尝试~/.ssh/id_rsa、~/.ssh/id_ecdsa、~/.ssh/id_ed25519这几个文件名如果你的密钥文件名很特殊比如叫my_server_key那必须通过ssh -i ~/.ssh/my_server_key userhost显式指定或者写进~/.ssh/config里否则客户端默认不会拿它去认证。还要检查公钥内容本身。id_ed25519.pub文件应该是完整的一行形如ssh-ed25519 AAAA... userhost。有时候在 Windows 上复制公钥到服务器时会出现换行符问题导致authorized_keys里这条记录被拆成两行或多行服务端解析时就会失败。一个简单操作是把公钥文件内容整个拷贝粘贴到服务器后检查一下是不是完整单行。最稳妥的方式是用ssh-copy-id命令自动完成这个拷贝它会自己处理格式和换行ssh-copy-id -i ~/.ssh/id_ed25519.pub userserver它会生成一条ssh-copy-id命令在首次连接时输入密码即可把公钥追加到服务端的authorized_keys同时把权限都设置好。2.4 用ssh -vvv看认证过程的每一帧画面如果前面几步都看起来正常但还是连不上那就进入“看直播”模式。用ssh -vvv userserver连接它会输出整个握手和认证过程的详细日志。重点看认证阶段有没有出现这样的关键行debug1: Offering public key: /home/me/.ssh/id_ed25519 ED25519 SHA256:xxxx debug1: Server accepts key: /home/me/.ssh/id_ed25519 ... debug1: Authenticated to server ([192.168.1.100]:22) using publickey如果你看到一直在Offering public key但后面没有Server accepts key说明服务端在authorized_keys里根本没找到这把公钥。如果出现了Server accepts key但最后还是Permission denied说明服务端找到了公钥但后续的签名验证失败了最常见的原因是客户端用的私钥和服务端存的公钥不是同一对。这两个区分非常关键能帮你把排查范围缩小一半。3. 高频场景实操复盘VSCode、Git、Jenkins、批量登录3.1 VSCode 连接远程服务器配置文件和 Windows 权限坑VSCode 的 Remote-SSH 插件让远程开发变得非常爽但它本质上还是调用本地的ssh命令去连接服务器。很多人第一次用就卡在ssh: permission denied (publickey)上其实跟插件没关系还是 SSH 本身的问题。VSCode 里的 SSH 配置统一写在用户目录下的~/.ssh/config文件里写法很简单Host my-ubuntu HostName 192.168.1.100 User ubuntu Port 22 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30IdentityFile这一行指定了私钥路径这是排查 VSCode 连接失败时最该检查的地方。如果你没有写IdentityFile客户端只会尝试默认文件名。还要注意Host后面的别名不能跟服务器真实地址混淆VSCode 连接时填的是Host后面那个别名比如ssh rootmy-ubuntu或直接在 Remote-SSH 插件里选择my-ubuntu。Windows 用户还有一个专属的经典坑bad owner or permissions on C:\Users\ThinkPad/.ssh/config。Windows 上的 OpenSSH 对config文件的权限要求很严格如果这个文件可以被其他用户访问直接报错拒绝。网上很多教程让改权限但没讲清楚怎么改我实测有效的做法是打开 PowerShell用 icacls 重置继承并只给当前用户读权限icacls C:\Users\把你的用户名写上\.ssh\config /inheritance:r /grant:r $env:USERNAME:(R)执行完再连一次基本就好了。如果你用的是 WSL 和 Windows 双环境注意~/.ssh路径可能指代不同位置VSCode 如果跑在 Remote-SSH 插件里它默认会去看 Windows 用户目录下的.ssh配置而不是 WSL 里的这一点特别容易让换了机器的同学怀疑人生。3.2 Git 与 GitLab/GitHub SSH 认证一个命令验证真相用 Git 拉代码时遇到ssh: permission denied (publickey)经常发生在刚配好 GitLab 或 GitHub 密钥之后。生成密钥本身很简单ssh-keygen -t ed25519 -C your_emailexample.com一路回车会在~/.ssh下生成id_ed25519和id_ed25519.pub。然后把.pub文件里的内容整个复制粘贴到 GitLab 的“SSH Keys”页面或 GitHub 的“Add SSH key”页面。这里有个很容易踩的坑复制的时候千万别只复制一部分也别用记事本打开后复制出奇怪的编码字符。最保险的方法是直接右键复制文件内容或者在终端里用cat ~/.ssh/id_ed25519.pub查看后复制。配置好之后验证 Git 连接用的是专门的命令不是ssh userhost。以 GitHub 为例ssh -T gitgithub.com成功的返回一般是Hi xxx! Youve successfully authenticated, but GitHub does not provide shell access.。看到这行说明 SSH 认证已经通了剩下有问题就是仓库权限的事了。如果返回Permission denied (publickey)那问题就出在 SSH 密钥本身跟 Git 的 user.name、user.email 没有半毛钱关系别去乱改全局配置。另外如果你同时使用公司和个人的 GitLab/GitHub 账号就涉及多密钥切换的问题。这时候~/.ssh/config就能帮上大忙Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal Host gitlab.company.com HostName gitlab.company.com User git IdentityFile ~/.ssh/id_ed25519_company这样 Git 在连不同域名时会自动选用对应的私钥不会再出现“同一台电脑两个账号互相打架”的情况。3.3 Jenkins 自动化部署密钥别硬塞在命令行里Jenkins 里跑自动化任务需要连远程服务器部署如果提示java.lang.IllegalStateException: Connection is not established!很容易让人误以为是代码问题其实根子多半在 SSH 密钥配置上。Jenkins 里正确的做法是走 Credentials 管理进入“凭据”页面添加一个“SSH Username with private key”类型的凭据把私钥内容PEM 格式也就是id_ed25519或id_rsa文件的内容粘贴进去然后配置 Publish Over SSH 或 SSH Agent 插件选择这个凭据去连接目标服务器。这里有两个高频出错点一个是私钥格式不对比如你在 Windows 上用的密钥是 PuTTY 的.ppk格式需要先用 PuTTYgen 导出成 OpenSSH 格式才能给 Jenkins 用另一个是私钥内容复制时带了多余的空格或换行导致解析失败。还有Jenkins 用的私钥对应的公钥必须已经写进目标服务器的authorized_keys里别以为 Jenkins 有私钥就够了这跟手工ssh连接是完全一样的逻辑。如果你遇到 “Connection is not established!”最优先排查三点目标服务器的 22 端口是否可达、私钥是否配对、目标服务器是否恰好禁用了password认证导致 Jenkins 尝试密码登录也被拒。我建议先在 Jenkins 所在机器上手动执行一遍ssh -i 私钥 userhost确认连通再回 Jenkins 里调配置能省很多时间。3.4 批量登录与免密分发别把密码写进脚本一次性管几十台机器的时候很多人会写个脚本循环sshpass批量执行命令。sshpass确实方便但直接把明文密码写进脚本或者历史记录里这本身就是个安全隐患。我的建议是先配置好密钥免密登录再用密钥去批量操作密码只出现在第一次分发密钥的那一刻。批量分发密钥的标准做法是循环执行ssh-copy-idfor host in 192.168.1.101 192.168.1.102 192.168.1.103; do ssh-copy-id -i ~/.ssh/id_ed25519.pub root$host done这个过程每台机器会提示你输入一次密码密码不会写入任何文件。密钥分发完以后后续的批量命令就可以直接通过ssh root$host uname -a来跑也可以在 ansible、pssh 等工具里直接使用当前用户的 SSH 认证不再需要密码。这里特别提醒一句批量场景下每台机器的authorized_keys都要确保权限正确~/.ssh目录本身也不能被组用户或其他用户写入否则StrictModes会直接拒绝认证。批量操作时如果某台机器突然失败优先怀疑是不是那台机器上的副本权限被复制脚本改坏了。4. 权限问题专题SSH 比你想的更“挑剔”4.1 权限标准与 StrictModes 机制很多人在permission denied (publickey)前面栽跟头不是因为公钥没配对而是因为权限设得太宽松。OpenSSH 服务端默认开启了StrictModes它会检查~/.ssh、authorized_keys、家目录的权限是不是“安全”的。如果目录或文件能被其他用户写那么服务端会认为这些文件可能被篡改过直接拒绝使用。这就像门卫发现访客登记表可以被路人随便涂改那肯定不敢放行。官方推荐的安全权限是这样的家目录本身不能是组可写或其他用户可写一般 755 或 700~/.ssh目录应该是 700~/.ssh/authorized_keys应该是 600 或 644但绝不能 666。如果你发现权限有问题执行chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chmod 755 ~ # 注意这里改成你自己家目录的合适权限别小看这些权限数字SSH 对它们很较真。还有一个常见坑是家目录的属主不对比如你把服务器数据盘挂载到了家目录导致家目录属主是root那也过不了检查。此时除了chmod还需要确认属主chown -R user:user ~/.ssh在 CentOS/RHEL 这类使用 SELinux 的系统上单纯改完权限还不够如果~/.ssh的 SELinux 上下文不对也可能被拒。这种时候需要用restorecon -R -v ~/.ssh把 SELinux 上下文恢复成标准值。这类问题在日志里通常表现为Authentication refused: bad ownership or modes或类似的提示一看日志就能对上。4.2 Windows 下 OpenSSH 的权限坑与修复思路Windows 下运行 OpenSSH 的权限机制和 Linux 差异很大经常报bad owner or permissions on C:\Users\xxx/.ssh/config。这个问题的本质是OpenSSH for Windows 认为config文件的 ACL 权限太开放可能允许其他系统用户读取出于安全考虑直接拒绝。修复思路就是把这个文件的所有权收紧到当前用户。我实测下来最稳定的命令是在 PowerShell 里执行icacls C:\Users\你的用户名\.ssh\config /inheritance:r /grant:r $env:USERNAME:(R)/inheritance:r表示移除所有继承的父级权限/grant:r表示只给当前用户读取权限。执行后可以再跑icacls C:\Users\你的用户名\.ssh\config查看权限列表确认里面只有你的用户名才放心。如果你用的是 Windows 自带的 OpenSSH 客户端却没有管理员权限去执行这些命令那也容易卡住——这种情况下建议直接用 WSL 里的sshWSL 的权限模型对 SSH 更友好我日常在 Windows 上做远程开发都是优先走 WSL 的。5. 排查手册现象 × 原因 × 解法速查表结合我自己的排障经验把最常见的几种情况整理成了一个速查表遇到问题可以直接对照着查现象大概率原因快速解法第一次连接就提示 Permission denied密码认证被禁用或 root 登录受限检查PermitRootLogin和PasswordAuthentication用密钥或控制台进入系统日志显示 no matching key exchange客户端和服务端 OpenSSH 版本差异过大升级服务端 OpenSSH或在客户端-o KexAlgorithmsdiffie-hellman-group1-sha1兼容旧算法日志显示 Failed publickey公钥不在authorized_keys或权限不对重新检查公钥内容、复制方式确认chmod 600 authorized_keys日志显示 bad ownership or modes~/.ssh或家目录权限过宽按上一节命令修正目录和文件权限VSCode 连接报 bad owner or permissionsconfig文件 ACL 权限过宽用icacls重置继承并给当前用户读取权限Jenkins 报 Connection is not established私钥格式不对或目标端公钥缺失检查 PEM 私钥格式、确认私钥对应公钥已存入authorized_keysWindows 下部分密钥能连、部分不能私钥未通过 ssh-agent 加载ssh-add -l查看必要时ssh-add手动把私钥加进 agent密钥明明存在但客户端不尝试文件不在默认位置或文件名特殊用ssh -i显式指定或写入~/.ssh/config的IdentityFile重启后密钥失效SELinux 上下文丢失或目录被挂载改动restorecon -R -v ~/.ssh检查家目录属主这个表解决不了所有问题但覆盖了 90% 以上的日常场景。我自己的排障习惯是先看日志再验-vvv然后对照速查表逐项排除。一圈下来几分钟内基本能定位到环节。最后再分享一个我一直沿用的习惯每次新配一台服务器我会固定做四件事——生成 ed25519 密钥、用ssh-copy-id推公钥、重启sshd、看一眼auth.log确认登录成功。这套流程听起来简单但真的能帮你躲掉 90% 的后续折腾。SSH 的错误提示虽然冷冰冰但它背后有一套非常清晰的规则理解了规则permission denied (publickey)就不再是拦路虎而只是一个提醒你“哪一步没做对”的路标。
返回列表