ARTICLE DETAIL

资讯详情

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

SSH免密登录常见报错排查:从ssh-copy-id到权限修复

SSH免密登录常见报错排查:从ssh-copy-id到权限修复 我相信不少人遇到过这个场面手里攥着服务器密码认认真真敲完ssh-copy-id userserver输入密码提示成功结果下一次ssh userserver还是照样问你密码或者更糟ssh-copy-id本身就直接甩你一句Permission denied (publickey,password)。再过一会儿连 VSCode Remote-SSH 都跟着报Bad owner or permissions on C:\Users\thinkpad/.ssh/config。这堆问题的根源往往不在“密码”或“公钥内容”而在于整个 SSH 认证链路里某个环节的权限、配置或平台差异。这篇文章就围绕ssh-copy-id的前前后后把高频报错按信息逐条拆开讲清楚每一步的排查方法和底层逻辑顺便带上ssh -vvv这类真正管用的诊断手段。适合刚配置密钥免密的同学也适合在 Windows 下折腾 OpenSSH、VSCode 远程开发时被权限问题反复摩擦的人。1. ssh-copy-id 到底帮你干了什么理解命令的四个动作1.1 一条命令背后的隐藏行为ssh-copy-id userhost看起来只是“把公钥拷过去”但实际它替你做了四件事第一在本机~/.ssh/下找默认公钥通常优先id_rsa.pub新版 OpenSSH 也会找id_ed25519.pub第二用密码或已有密钥登录远程主机第三在远程主机的用户家目录下创建~/.ssh目录第四把公钥内容追加到~/.ssh/authorized_keys文件末尾。这四步任何一步出问题后面的免密就不可能成立。很多人的误区是我手动用scp id_rsa.pub userhost:~/.ssh/authorized_keys不是一样吗不一样。scp是直接覆盖如果你之前已经加过别的公钥一覆盖就全没了而ssh-copy-id用的是追加这也是它能被反复安全执行的原因。理解这四步之后报错就有了解释方向命令找不到是本机工具缺失登录失败是认证环节目录创建或文件追加失败是远程权限权限不对是后续sshd校验阶段直接拒绝。1.2 权限要求不是玄学目录和文件的真实权限标准为什么公钥明明写进了authorized_keysssh还是要密码绝大多数原因是权限不达标。OpenSSH 的sshd在启用StrictModes yes默认开启时会严格检查三个地方的权限远程用户家目录~属主必须是登录用户且不能对 group 或其他用户可写。比如/home/user权限是777那 sshd 直接拒绝使用该用户的 authorized_keys。远程~/.ssh目录权限应该是700也就是只有属主可读写执行。远程~/.ssh/authorized_keys文件权限应该是600属主可读写其他人什么都别碰。ssh-copy-id命令默认会做chmod 700 ~/.ssh和chmod 600 ~/.ssh/authorized_keys但如果你之前手动创建过.ssh目录或者用scp复制过文件权限可能已经被改歪了再执行一次ssh-copy-id未必会帮你修正已有文件的权限。这时候我一般直接手动确认ssh userhost ls -ld ~ ~/.ssh ~/.ssh/authorized_keys ssh userhost chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys需要注意的是家目录权限如果太松比如/home/user是755是没问题的但如果是777就必须改回755或更严格。这个检查对 CentOS、Ubuntu、麒麟等 Linux 发行版基本通用。1.3 为什么“复制公钥文件”不行必须“追加”我见过不少人这样操作先用ssh-keygen生成密钥然后用cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys手动追加这个方向是对的。但另一些人用scp把本机公钥直接覆盖到远端authorized_keys如果这台服务器只有你这一个用户那还能用一旦服务器里存了其他机器的公钥覆盖之后那台机器立刻失效。ssh-copy-id的追加语义就是要避免这种问题。还有一个小细节ssh-copy-id支持指定公钥文件如果你本机有多个密钥默认可能选错。这时候用-i显式指定后面排查也更有针对性ssh-copy-id -i ~/.ssh/id_ed25519.pub userhost2. 高频报错按图索骥从错误信息到根因2.1 ssh-copy-id: command not found这个报错在 macOS 和 Windows 上最常见。macOS 虽然自带ssh客户端但很长一段时间默认不装ssh-copy-id需要自己用 Homebrew 安装brew install ssh-copy-idWindows 10/11 自带的 OpenSSH 客户端同样不带ssh-copy-id。如果你的系统里装了 Git for Windows 或 WSL里面可能有这个命令但原生 PowerShell 和 cmd 里没有。这种情况最简单的替代方案是手动复制公钥type $env:USERPROFILE\.ssh\id_rsa.pub | ssh userhost mkdir -p ~/.ssh chmod 700 ~/.ssh cat ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys这里用mkdir -p保证.ssh目录存在且不覆盖已有内容chmod顺手把权限修正最后cat 追加而不是覆盖。手动流程熟记之后比依赖ssh-copy-id更通用尤其是遇到老设备或嵌入式系统时很管用。2.2 Permission denied (publickey,password)从三种路径排查这个报错表示服务器拒绝了你提供的所有认证方式既没有接受公钥也没有接受密码。逐层排查通常落在三处第一本机密钥文件权限有问题。OpenSSH 对私钥文件id_rsa、id_ed25519要求权限为600如果你不小心chmod 644了客户端会直接忽略这个私钥日志里会出现bad permissions字样。修复chmod 600 ~/.ssh/id_rsa ~/.ssh/id_ed25519第二远程sshd配置禁止了密码认证。检查/etc/ssh/sshd_config里的PasswordAuthentication是否为yes同时看PermitRootLogin。云厂商默认镜像经常把 root 的密码登录关掉如果你在本地用 root 执行ssh-copy-id就会看到这个报错。解决是用一个有 sudo 权限的普通用户登录再把密钥加到 root 或自己的authorized_keys里。第三公钥没被正确追加到authorized_keys或者追加之后权限不对。这个场景我在后面第 5 章专门用案例展开。2.3 Connection timed out / Connection refused网络层和服务层分开查Connection timed out和Connection refused经常被混为一谈实际含义完全不同。timed out表示包发出去了对方没回应常见原因目标 IP 不通、防火墙丢弃了 22 端口、跨网段路由不对、云安全组没放行。refused表示对方网络可达但端口上没人监听可能 sshd 没启动、监听在其他端口、或者被 fail2ban 临时封禁。我一般按这个顺序排查ping host nc -vz host 22 telnet host 22 systemctl status sshd ss -tlnp | grep sshnc -vz host 22只要显示succeeded说明端口通了问题大概率在认证层如果端口不通先别急着折腾公钥把网络和安全组搞定再说。VSCode Remote-SSH 报Connection timed out时同样先用这几条命令判断而不是反复重装插件。2.4 Host key verification failed 与 REMOTE HOST IDENTIFICATION HAS CHANGEDHost key verification failed意味着远程主机的 host key 和你本地known_hosts里记录的不一致。最常见的触发场景是服务器重装系统、更换镜像、或者容器重建后 IP 复用了。这时候不要直接删掉整个known_hosts只要删除冲突的那一条就行ssh-keygen -R 192.168.1.100 ssh-keygen -R server.example.com如果你想在脚本或首次连接时自动接受新的 host keyOpenSSH 7.6 以后可以用ssh -o StrictHostKeyCheckingaccept-new userhostssh-copy-id同样支持透传-o参数ssh-copy-id -o StrictHostKeyCheckingaccept-new userhost但注意accept-new只接受“新”的 host key如果known_hosts里已有冲突记录它照样会失败所以脚本里要么先ssh-keygen -R要么接受StrictHostKeyCheckingno的风险。生产环境不建议长期使用no这个选项只适合一次性初始化场景。2.5 Bad owner or permissions on C:\Users\thinkpad/.ssh/config这个报错是 Windows 下 OpenSSH 的经典问题VSCode Remote-SSH 用户应该都见过。Win10/11 自带 OpenSSH 对config文件权限要求非常严格默认情况下用户主目录和.ssh目录可能带着从父目录继承来的、属于多个用户或组的 ACL 权限OpenSSH 觉得不安全就直接拒绝。处理方式打开文件属性 - 安全 - 高级先点“禁用继承”把从上层继承的权限全部清掉然后只保留当前用户SYSTEM和Administrators的完全控制权。如果想用命令行PowerShell 里执行icacls $env:USERPROFILE\.ssh\config /inheritance:r /grant:r $env:USERNAME:F icacls $env:USERPROFILE\.ssh\known_hosts /inheritance:r /grant:r $env:USERNAME:F顺手把整个.ssh目录也处理一遍避免id_rsa、id_rsa.pub后续再报权限问题。改完之后重新打开 VSCode执行Remote-SSH: Kill VS Code Server on Host再重连能解决大部分 Windows 上的 SSH 配置异常。顺带一提plink 用户PuTTY 的命令行组件如果出现类似登录后无法执行 sudo 的问题通常不是权限而是没有分配 tty加-t参数即可。3. 带上 ssh -vvv 做一次“案发现场”复查3.1 -v、-vv、-vvv 分别能看到什么很多人排错 SSH 全凭猜其实 OpenSSH 自己就带了一个非常完整的诊断模式根据-v的数量决定日志详细程度。ssh -v输出连接过程的关键节点包括正在尝试的认证方法、读取了哪个密钥文件ssh -vv会增加更细的协商细节ssh -vvv则进入“显微镜”模式把密钥交换、报文类型、远端返回的状态全打印出来。实际操作中我建议先用-v看大方向再对可疑环节用-vvv追细节。不要在平时登录也加-vvv那会刷屏刷到你找不到重点。ssh -vvv userhost日志里值得关注的节点比报错信息本身更准确因为它会告诉你问题发生在“读取本地密钥”还是“远端验证”阶段。3.2 一次成功认证的日志长什么样如果一切正常ssh -vvv的尾部会看到类似这样的流程debug1: Next authentication method: publickey debug1: Offering public key: /home/user/.ssh/id_ed25519 ED25519 SHA256:xxxx debug3: send packet: type 50 debug2: we sent a publickey packet, wait for reply debug1: Server accepts key: /home/user/.ssh/id_ed25519 debug3: send packet: type 21 debug1: Authentication succeeded (publickey).关键在Server accepts key这行。它表示远程 sshd 已经在你提供的公钥里找到了匹配项并且完成了签名校验。只要出现这一行说明authorized_keys的内容没问题前面折腾的权限大概率也正确。如果卡在Offering public key之后迟迟没有回应或者直接返回password认证说明远端根本不认这把公钥。3.3 失败日志的关键词判读表下面这张表是我排障时实际用到的速查表按日志关键词定位下一步动作日志关键词含义下一步操作Permission denied (publickey)远端不接受提供的公钥检查 authorized_keys 内容、.ssh 权限、sshd_configbad permissions本地私钥或 config 权限不对chmod 600 id_rsaWindows 修 ACLConnection refused22 端口无服务监听启动 sshd确认端口Operation timed out网络层不可达查防火墙、路由、安全组no matching key exchange method客户端与服务器算法套件不匹配指定 KexAlgorithms 或升级 OpenSSHHost key verification failedknown_hosts 记录冲突ssh-keygen -R 删除旧记录Server accepts key公钥认证本身已成功继续排查后续如 tty、sudo 权限这表的逻辑是先把报错归类到网络层、服务层、认证层或权限层再决定动哪里。很多人在网络层都没通的情况下反复生成密钥纯属浪费时间。4. Windows 平台下 ssh-copy-id 的完整替代方案4.1 Windows 现状自带 OpenSSH但默认没有 ssh-copy-idWindows 10 1809 之后的系统可以通过“设置 - 应用 - 可选功能”安装 OpenSSH 客户端安装后ssh、scp、ssh-keygen都可用但唯独没有ssh-copy-id。所以在 Windows 的原生终端里执行ssh-copy-id第一反应应该是“命令不存在”而不是想歪了。如果你装了 Git Bash里面可能会顺带提供一个ssh-copy-id脚本但不同版本的 Git for Windows 行为不太一样有的能找到有的没有不建议把它当作稳定依赖。解决思路也很简单把ssh-copy-id背后的逻辑手动执行一遍。这个方案绕开了命令是否存在的问题也更容易处理像 plink、跳板机这类特殊环境。4.2 手动复制公钥的标准命令在 PowerShell 里手动复制公钥的完整命令是type $env:USERPROFILE\.ssh\id_rsa.pub | ssh userhost mkdir -p ~/.ssh chmod 700 ~/.ssh cat ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys如果远程系统比较老不支持chmod比如部分嵌入式设备或老交换机可以把后面的chmod去掉只保留mkdir -p和cat 。需要注意type是 PowerShell 里读取文件内容的命令CMD 里也可以用功能等同于 Linux 的cat。如果你的默认公钥是id_ed25519.pub记得改文件路径。这里有个经验如果远端是 Windows 的 OpenSSH Server那路径和权限逻辑又不一样了authorized_keys通常在C:\Users\用户名\.ssh\authorized_keys追加命令里的chmod可能不生效需要改用icacls给当前用户授予权限。4.3 Windows 下 .ssh 目录权限的修复Windows 的 OpenSSH 对权限的判断沿用 Unix 的严格思路但实现上完全依赖 Windows ACL。最常见的就是config文件继承了C:\Users\thinkpad目录的权限导致当前用户之外的其他账号也有读取权OpenSSH 直接拒绝。通过图形界面修复的路径是右键config- 属性 - 安全 - 高级 - 禁用继承把继承的权限转换为显式权限或直接删除- 删除多余条目 - 只保留当前用户和 SYSTEM 的完全控制。操作完以后known_hosts如果有类似问题同样处理。如果嫌界面操作啰嗦PowerShell 里用icacls一条命令搞定icacls $env:USERPROFILE\.ssh\config /inheritance:r /grant:r $env:USERNAME:F细心的朋友会发现$env:USERNAME在 PowerShell 里是当前用户名但如果你把这段放进了双引号里PowerShell 会先展开变量再传给icacls所以路径是正常的。不要在 CMD 里直接跑%USERNAME%的版本除非你想踩转义坑。4.4 VSCode Remote-SSH 与 config 文件权限VSCode Remote-SSH 本质就是对~/.ssh/config的解析加一个ssh客户端进程。它报Bad owner or permissions根因就是上一节说的 Windows ACL 问题。修完权限之后还需要在 VSCode 里执行Remote-SSH: Kill VS Code Server on Host因为旧的 SSH Server 进程还可能带着出错时的状态不杀掉重连可能继续报错。顺便说一句VSCode 里配置多主机可以用config的别名和IdentityFile但 IdentityFile 路径在 Windows 上最好是绝对路径比如C:\Users\thinkpad\.ssh\id_ed25519不要写成~/.ssh/id_ed25519这种相对格式某些情况下解析会出问题。填绝对路径配合上一节的权限修复VSCode 连服务器基本不会再卡在“权限”这关。5. 三个真实排错案例从报错到免密登录5.1 案例一公钥明明加了还是提示输入密码有位朋友的 Ubuntu 服务器按网上教程把公钥内容贴进了authorized_keys但每次ssh还是问密码最后翻/var/log/auth.log才发现一行关键日志Authentication refused: bad ownership or modes for directory /home/user原因是他之前手动把整个家目录chmod -R 777过.ssh目录和authorized_keys也跟着变成了777。SSH 的StrictModes检测到这种开放权限会直接拒绝使用这个文件里的任何公钥哪怕内容完全正确。修复命令很简单chown -R user:user /home/user/.ssh chmod 700 /home/user/.ssh chmod 600 /home/user/.ssh/authorized_keys chmod 755 /home/user这里强调一个细节authorized_keys文件的所有者也必须是登录用户如果你用 root 帮忙把文件chown成了 root普通用户登录时 sshd 一样会拒绝。遇到这个报错时用ls -la确认.ssh下文件的所有者。5.2 案例二root 登录被禁用ssh-copy-id 直接报 Permission denied另一个典型场景是云厂商提供的 Ubuntu 镜像默认只允许普通用户登录root 的PermitRootLogin是prohibit-password也就是禁止 root 密码登录但允许密钥登录。你直接用ssh-copy-id rootserver如果 root 没有密码系统会直接拒绝密码认证报Permission denied (publickey,password)。正确做法是先用有 sudo 权限的普通用户登录比如 ubuntu 或 adminssh-copy-id ubuntuserver ssh ubuntuserver sudo -i然后编辑/etc/ssh/sshd_config把PermitRootLogin改回prohibit-password而不是yes这样既允许密钥登录又保留密码登录的部分限制。改完之后systemctl reload ssh不要改成yes就把 root 密码登录打开安全上完全没有必要。如果你确实需要在脚本里以 root 身份执行远程命令建议用普通用户加sudo的组合而不是强行开放 root 登录。5.3 案例三非 22 端口、多台服务器批量执行ssh-copy-id对非标准端口的使用有个容易踩的坑-p只能放在用户和主机之前不能跟在命令末尾否则会被当成远程命令的一部分。正确写法ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub userhost批量给多台服务器推送公钥时写一个简单的 for 循环最常见for host in 192.168.1.101 192.168.1.102 192.168.1.103; do ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub user$host done执行每台机器时还是会要求输入一次密码除非你已经配置了免密。如果非要全自动可以用sshpass -p 密码 ssh-copy-id ...但明文密码会出现在 shell history 和进程列表里生产环境慎用。我个人的建议是如果机器数量不多循环里手动输密码最稳妥如果机器数量很大走配置管理工具ansible 之类集中下发公钥而不是在裸 shell 脚本里硬编码密码。批量场景下私钥如果设置了 passphrase每连一台新机器都会让你输入一次私钥口令。这时候先启动ssh-agent并添加密钥eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519之后ssh和ssh-copy-id会自动从 agent 取密钥进行签名不需要反复输入口令。这个方法对 VSCode Remote-SSH 同样有效Windows 下只需要在 PowerShell 里先跑ssh-agent服务并ssh-add。6. 密钥认证打通之后的维护和调优6.1 多主机多密钥的目录规划密钥多了以后不建议把所有私钥都扔在~/.ssh下然后用默认文件名那样ssh客户端会挨个尝试浪费时间也可能触发服务器端的失败计数。更规范的做法是在~/.ssh/config里为不同主机定义别名和指定密钥Host aliyun HostName 203.0.113.10 User ubuntu Port 22 IdentityFile ~/.ssh/id_ed25519_aliyun Host office HostName 10.0.0.5 User admin Port 2222 IdentityFile ~/.ssh/id_ed25519_office配置完成后直接ssh aliyun就能登录。这个 config 文件的权限同样重要在 Linux 下执行chmod 600 ~/.ssh/config在 Windows 下按第 4 章的 ACL 方法处理。多主机场景里最忌讳的是把几十台服务器密码都记在文本文件里用 key 加 config 的方式既方便又安全。6.2 known_hosts 的维护known_hosts是一个会随着时间不断膨胀的文件服务器重装、IP 复用都会导致 host key 变化。平时我在重装系统后都会执行ssh-keygen -R删除对应主机的旧记录而不是把整个known_hosts删掉。多用户环境下可以开启HashKnownHosts yes让 known_hosts 里的主机名和 IP 以哈希形式保存避免服务器地址泄露给拿到文件的人。6.3 ssh 断开与长任务处理tmux 和断点续训密钥免密只是第一步真正远程跑训练任务、数据迁移、部署脚本的人最烦的是 SSH 突然断开后任务跟着挂掉。热搜里的“断点续训”本质不是 SSH 的问题而是任务进程没有脱离 SSH 会话。解决方案是在远程主机上用tmux或screen把任务挂在后台ssh userserver tmux new -s train python train.py断线重连后用tmux attach -t train回到原会话。这是我最推荐的方式比nohup ... 直观得多因为你能随时看到输出还能丢给别的同事接管。再配合客户端侧的心跳保活把~/.ssh/config里加上Host * ServerAliveInterval 60 ServerAliveCountDown 3每 60 秒发一个 keepalive 包连续 3 个没回应才断线能大幅减少“办公室网络抖动一次远程训练就断掉”的尴尬。6.4 sshd 配置的预防性建议走到免密成功后很多人就再也不看sshd_config了。我建议在初始化服务器时就做几件预防性的事确认PubkeyAuthentication yes是开启的PasswordAuthentication按安全基线决定不用的场景直接关掉MaxAuthTries设置一个较小的值比如 3防止有人暴力试探LoginGraceTime不要给太长默认 120 秒可以接受。修改sshd_config之后有一个安全操作习惯先开一个额外的 SSH 会话保持不动然后执行systemctl reload ssh再开新窗口验证验证成功后再关老会话。这样即使配置写错了你也不至于把自己锁在机器外面。我吃过一次亏改完PermitRootLogin直接重启 sshd结果新配置语法错误老连接也断了最后只能靠云厂商的控制台 VNC 进去救。从那以后备份配置、保留逃生会话、reload而不是restart这三步成了我改 sshd 的固定流程。另外老设备和老系统的兼容问题也值得一提。有些华为交换机或老款 ARM 设备只支持旧版本的 SSH 算法新客户端连上去会报no matching key exchange method。这时候可以在命令里临时指定算法定位问题但最终还是要升级系统里的 OpenSSH而不是长期用弱算法凑合。安全性和兼容性的平衡永远是先保证前者。
返回列表