SSH免密登录,应该是Linux运维里最常用也最容易被小问题卡住的一个功能。明明公钥已经放进去了,却一直提示Permission denied;明明权限也调了,known_hosts也清了,SELinux也看了,最后还是没有通。这类问题看起来简单,真正排查起来却能让人耗掉整个下午。很多同行排查时有个共同的误区:习惯性地把关注点放在“文件权限”上,却忽略了SSH自身的权限校验规则、客户端指纹缓存,以及SELinux上下文这几层隐性因素,结果绕了一大圈才发现是些平时不太注意的细节。
这篇文章把我这些年实际踩过、也帮别人解决过的坑,按“权限位错误、known_hosts冲突、SELinux干扰”这三条主线整理出来。如果你正被免密登录问题卡住,照着这套思路走一遍,基本能定位到原因;如果你还没遇到,也建议收藏备用,这类问题几乎每个运维都会碰到,只是早晚问题。
1. 免密登录的完整链路与第一排查入口
1.1 一次免密登录到底经历了什么
很多人配置免密登录,就是执行一条ssh-copy-id,然后把密码一输,之后再登录就不需要密码了。但这里的“免密”,并不是真的一直免密,而是把“输密码”替换成了“验签名”。
整个过程大致是这样:客户端先在自己的~/.ssh/目录下生成一对密钥,一个是私钥,一个是公钥。公钥会被写入服务器上对应用户的~/.ssh/authorized_keys文件里。当客户端发起SSH连接时,服务器会找到该用户的authorized_keys文件,用里面保存的公钥去验证客户端拿出来的私钥签名,验证通过就放行。这就像你提前给门卫留了一张带照片的通行证模板,等你到门口的时候,门卫拿着你出示的证件和模板比对,一致就让你进,而不是每次进门都现场重新登记。
这个链路里任何一环出问题,最终表现都可能只是“还是提示输入密码”或者“Permission denied”。但具体是哪一环,需要看服务端日志才能确认。所以我不太建议直接在客户端盲试参数,第一步应该先去服务端看SSH日志。
1.2 先看日志,再动配置
服务端日志的位置和发行版有关。RedHat系一般在/var/log/secure,Debian系一般在/var/log/auth.log。如果服务器使用systemd,也可以直接实时跟踪sshd日志:
journalctl -u sshd -f想看到更详细的原因,建议临时把服务端的日志级别调高。编辑/etc/ssh/sshd_config,找到LogLevel,改成VERBOSE,重启sshd再复现一次问题:
# /etc/ssh/sshd_config LogLevel VERBOSE systemctl restart sshd然后重新从客户端发起连接,再回服务端看日志。常见的几种报错长这样:
bad ownership or modes for directory /home/userAuthentication refused: bad ownership or modes for file /home/user/.ssh/authorized_keysConnection closed by authenticating usererror: sshd: no matching key exchange method
把这些日志内容拿到手里,后面的排查就有方向了。很多人一上来就反复改authorized_keys文件、反复重启服务,其实都没有日志来得直接。
2. 权限位错误:最常见也最隐蔽的拒绝原因
2.1 为什么SSH对权限这么敏感
SSH对公钥认证相关文件的权限检查非常严格。因为authorized_keys这个文件本质上是一把“钥匙”,如果任何人都能改,那服务器就等于裸奔。所以OpenSSH会从用户家目录开始,逐层检查所有相关目录和文件的属主、属组、权限位,任何一层出现“组可写”或者“其他用户可写”,它都会直接拒绝认证。
这个机制初看有点“不讲理”,但仔细想很有必要。比如某台服务器上有多个用户,如果用户A的家目录权限是777,那用户B就能在A的家目录里塞自己的公钥,或者直接替换authorized_keys文件。SSH为了防止这类情况,宁可把放宽权限的连接全部拒绝,也不冒险放行。
这也是为什么很多新手第一次配置免密登录时会非常困惑:明明公钥写对了,命令也执行成功了,就是登录不进去。因为问题根本不在公钥内容上,而在于保存公钥的这个人门前的“走廊权限”不合格。
2.2 标准权限值到底是多少
直接给出一套稳妥的推荐值。用户家目录、~/.ssh目录、authorized_keys文件三者,每一层都不能有任何“组写”或“其他写”权限。
chmod 755 /home/username # 家目录,记住不能 g+w, o+w chmod 700 /home/username/.ssh # .ssh 目录 chmod 600 /home/username/.ssh/authorized_keys chmod 644 /home/username/.ssh/*.pub # 公钥文件本身可宽松需要注意的是,家目录不一定要755,700也可以。但绝对不能出现属于组或其他人可写的情况。~/.ssh目录的推荐值是700,有些环境用750也能正常工作,但如果你拿不准,直接按700来最安全。authorized_keys文件则必须是600或更严格,属主必须是该用户自己,不能是root,也不能是其他用户。
2.3 一个典型案例:路径中间层权限违规
有朋友问过我一个问题:他的authorized_keys权限是600,家目录和.ssh都是700,为什么SSH还是报bad ownership or modes?
后来一查,他的家目录是通过挂载盘映射过来的,路径是/data/username,但问题出在父目录/data上。/data的权限是777,而SSH的权限检查是从根路径往下逐层进行的,路径上任何一级目录权限过宽,都会导致认证被拒。这个时候你只盯着~/.ssh/authorized_keys看,永远发现不了问题。
排查这种路径权限问题有个很好用的命令:namei。它会把完整路径里每一层的权限和属主都列出来,一眼就能看到问题出现在哪一级。
namei -l /data/username/.ssh/authorized_keys输出类似:
f: /data/username/.ssh/authorized_keys dr-xr-xr-x root root / drwxrwxrwx root root /data drwx------ username username /data/username drwx------ username username /data/username/.ssh -rw------- username username /data/username/.ssh/authorized_keys看到/data是drwxrwxrwx,问题就定位了。修复也很直接:
chmod 755 /data顺便说一句,namei这个命令平时用得不多,但在排查“路径上哪一层权限不对”的时候,比一层层ls -ld效率高很多,尤其是遇到家目录嵌套多层、挂载盘、软链接场景时特别实用。
2.4 修改完权限后建议验证一下
改完权限后,不要急着说“好了”,建议直接在服务器本地用该用户身份测试一遍,排除客户端和网络因素:
sudo -u username ssh-keygen -y -f /home/username/.ssh/authorized_keys这个命令本身不是用来验证权限的,更直接的方式是看服务端日志里是否不再出现bad ownership or modes。还有一个小技巧:把客户端日志级别开到最大,也就是用ssh -vvv连接。-vvv会输出从连接建立、密钥交换到最终认证的所有过程,虽然内容多,但里面会明确显示服务端拒绝了什么、服务端返回值是什么。
3. known_hosts 冲突:当服务器指纹与记录不一致
3.1 known_hosts 到底在防什么
每次SSH连接服务器时,客户端都会把服务器的公钥指纹保存到~/.ssh/known_hosts文件里。下次再连接同一台服务器时,客户端会先取出本地记录和服务端当前给出的公钥进行比对。一致就继续,不一致就直接警告并中断连接。
这个机制是为了防止中间人攻击。假设有人伪造了一台服务器,劫持了你的流量,客户端无法凭肉眼知道对面是不是原来的服务器,但它可以检查公钥指纹。只要指纹对不上,客户端就会拒绝继续连接。known_hosts文件本质上就是客户端的“信任数据库”。
3.2 为什么会提示REMOTE HOST IDENTIFICATION HAS CHANGED
最常见的known_hosts冲突场景是:一台服务器重装系统、容器重建、或者云主机被销毁后重新分配了IP。新系统的SSH主机密钥和之前完全不同,而你本地known_hosts里还保留了旧记录。这时连接会被卡在指纹校验阶段,报错如下:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! ... Host key verification failed.遇到这个提示,很多人的第一反应是“服务器是不是被入侵了”。其实大多数情况下,是服务器本身发生了重建或重装。如果这台服务器是你自己刚重装过的系统,那就放心清理本地记录即可。
3.3 精准清理 known_hosts 中的单条记录
清理官方推荐的方式是使用ssh-keygen -R,它会从known_hosts中移除对应主机的所有条目:
ssh-keygen -R 192.0.2.10 ssh-keygen -R myserver.example.com如果是非22端口,需要注意格式。known_hosts中带端口的主机名会用方括号括起来,所以-R参数也需要写成方括号形式:
ssh-keygen -R "[192.0.2.10]:2222"这里有个很容易踩的坑:有人会直接手动编辑known_hosts文件,删掉对应行,但没注意同一主机可能会同时存在“IP记录”和“主机名记录”。如果之前用Host别名配置连过、后来又用IP连过,known_hosts里就可能同时有两条记录,只删一条依然报冲突。所以更稳妥的做法是用ssh-keygen -R分别清理IP和主机名两条记录,或者干脆查看一下当前有哪些条目:
ssh-keygen -F 192.0.2.10-F用来查找指定主机在known_hosts中的记录。查到几条,就删干净。
3.4 StrictHostKeyChecking 应该怎么设置
网上有一类文章为了省事,会建议把StrictHostKeyChecking设为no,甚至有人直接写ssh -o StrictHostKeyChecking=no user@host。这样确实不会因为指纹变更而报错,但同时也等于放弃了防中间人攻击的屏障。我个人非常不建议在生产环境这么做。
对于自动化脚本和批量执行场景,比较合理的折中方案是accept-new:
StrictHostKeyChecking accept-new这个模式的含义是:对于known_hosts里不存在的“新主机”,自动接受并写入记录;但如果known_hosts里已经存在的主机,其指纹发生了变化,依然会拒绝。这样既不影响首次连接的自动化体验,又保留了已有主机指纹的校验能力。
另外补充一点:如果你需要批量预置指纹,可以先手动获取服务器的公钥指纹,再写入客户端的known_hosts。获取方式可以用ssh-keyscan:
ssh-keyscan -p 2222 192.0.2.10拿到结果后,放到脚本或者CMDB系统里统一下发,这样客户端第一次连接就不用交互确认了。
4. SELinux 干扰:权限正常却仍失败的隐形原因
4.1 SELinux 如何影响 SSH 免密
有些场景下,权限位已经全部正确,known_hosts也没有冲突,客户端日志和服务端日志都没有明确报错,但免密登录就是不成功。这时候就要考虑SELinux是否在强制模式下拦截了sshd对authorized_keys文件的操作。
SELinux是Linux内核层面的访问控制机制,它比传统的“用户/组/权限”更细一层。文件不仅有属主和权限位,还有一个“安全上下文”标签。SSH守护进程运行在sshd_t这个域里,它想读取用户的authorized_keys文件时,不仅要求权限位通过,还要求文件的安全上下文类型与sshd的策略匹配。
正常情况下,用户家目录下的.ssh目录和authorized_keys文件,应该带有ssh_home_t这个类型标签。如果文件是通过cp -r从别处拷贝过来的、或者从备份迁移恢复的、或者在一个奇怪的挂载场景下生成的,类型标签就可能变成其他值,比如admin_home_t、default_t、usr_t等。一旦类型不匹配,即使权限位完全正常,sshd还是会被SELinux拒绝读取。
4.2 怎么看安全上下文和拒绝记录
查看文件的安全上下文用ls -Z:
ls -Z /home/username/.ssh/authorized_keys正常输出类似于:
unconfined_u:object_r:ssh_home_t:s0 /home/username/.ssh/authorized_keys如果看到类型不是ssh_home_t,比如是admin_home_t,那基本可以确定问题就出在这里。
SELinux的拒绝记录会写入审计日志。用ausearch可以快速查询近期与被拒绝内容相关的AVC记录:
ausearch -m AVC -ts recent输出里一般能看到类似于这样的关键信息:
type=AVC msg=audit(...): avc: denied { read } for pid=xxxx comm="sshd" name="authorized_keys" scontext=system_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=unconfined_u:object_r:admin_home_t:s0 tclass=file注意看scontext是sshd_t,tcontext是admin_home_t,这就说明sshd进程试图读取authorized_keys文件,但SELinux策略不允许sshd_t访问admin_home_t类型的文件。日志里这一串信息比任何猜测都准确。
4.3 修复安全上下文:restorecon 优先,不要一关了之
修复思路很简单:把相关文件的SELinux类型恢复为默认类型。首选命令是restorecon,它会根据系统默认的文件上下文规则重建标签:
restorecon -RF /home/username/.ssh-R表示递归处理,-F表示强制重置,不保留原有上下文。执行完再查看一次:
ls -Z /home/username/.ssh/authorized_keys如果类型已经变回ssh_home_t,再重新发起连接测试。
这里要特别强调一点:不要遇到SELinux相关问题就setenforce 0或者改/etc/selinux/config把SELinux关掉。关闭SELinux虽然能立刻“解决问题”,但等于把Linux最基础的一道安全防线撤掉了,生产环境尤其不建议。setenforce 0最多只能用来做一个临时验证,确认问题到底是不是SELinux引起的,确认后还是要恢复强制模式,再通过正确的方式去修复上下文或者调整布尔值。
4.4 注意挂载目录和网络文件系统场景
如果用户家目录不在本地磁盘上,而是在独立的挂载分区、NFS网络文件系统或者容器卷里,SELinux的上下文问题会更隐蔽。因为这种场景下,restorecon有时并不能正确恢复类型,或者文件系统本身不支持SELinux标签持久化。
判断方法还是ls -Z,如果文件系统不支持标签,输出会出现?之类的占位符号,或者看不出正常的ssh_home_t。这种情况下,可以检查一下是否有对应的SELinux布尔开关来控制sshd对这类文件系统的访问。查询命令是:
getsebool -a | grep ssh输出里会列出和SSH相关的布尔值。如果家目录在NFS之类的网络文件系统上,通常会有一个专门允许ssh访问网络家目录的开关,把它打开再验证连接即可。这类场景下,日志里的ausearch -m AVC依然是最可靠的定位依据。
5. 综合定位路径与问题速查
5.1 四步定位法
当一次免密登录失败时,我建议按照固定的顺序排查,而不是每次从头乱试。
第一步,先看服务端日志。大多数问题在日志里都会留下明确线索,比如bad ownership or modes指向权限位,AVC记录指向SELinux,而公钥校验失败会显示类似Failed publickey之类的信息。日志看不明白时,再打开LogLevel VERBOSE复现一次。
第二步,检查权限位。用namei -l把家目录到authorized_keys的完整链路拉出来,确认每一层的属主和组写权限是否合规。这一步可以把覆盖范围从最终文件扩展到整个路径。
第三步,检查known_hosts。如果客户端报的是Host key verification failed或REMOTE HOST IDENTIFICATION HAS CHANGED,就按ssh-keygen -R清理并重新确认指纹。即使没报错,也可以顺手确认一下客户端是否因为known_hosts文件本身权限太开放而被忽略。
第四步,检查SELinux。用ls -Z确认.ssh和authorized_keys的上下文类型,用ausearch -m AVC -ts recent查看有无被拒绝记录。尤其在权限位看起来完全正常但连接仍然失败的时候,这一步往往能解决问题。
5.2 问题对照速查表
| 症状 | 可能原因 | 典型日志线索 | 修复方式 |
|---|---|---|---|
| 提示输入密码 | authorized_keys内容不对、公钥未追加成功 | Failed publickey for user | 检查公钥内容,确认是ssh-rsa/ssh-ed25519开头且无换行 |
| 服务端日志报bad ownership or modes | 路径上某层目录权限过宽 | bad ownership or modes for directory | chmod 755 /home/user、chmod 700 ~/.ssh、chmod 600 authorized_keys |
| 客户端提示HOST IDENTIFICATION HAS CHANGED | known_hosts中旧指纹与新服务器不匹配 | 客户端界面直接提示 | ssh-keygen -R 目标主机 |
| 公钥认证失败但权限正常 | SELinux上下文类型不正确 | AVC denied、tcontext=admin_home_t | restorecon -RF /home/user/.ssh |
| SSH连接非常慢或卡住 | 服务器DNS反向解析、客户端GSSAPI | 日志显示reverse mapping checking getaddrinfo | 服务端配置UseDNS no、客户端关闭GSSAPIAuthentication |
| 第一次连接能成功,之后开始失败 | 服务器主机密钥更换,known_hosts未更新 | 同known_hosts冲突 | 清理known_hosts对应记录后重连 |
5.3 几个容易忽略的低级问题
除了上面三条主线,免密登录失败还有一些非常基础但容易被忽略的坑。
比如公钥粘贴错误。有些平台复制公钥时会出现多余换行或格式被破坏,导致authorized_keys里出现两行内容,第一行是空行或不完整的公钥。SSH解析时会认为公钥格式不合法,直接跳过。正确做法是保证authorized_keys里每一行都恰好是一个完整的公钥,以ssh-rsa或ssh-ed25519开头,以注释结尾。
比如公钥追加但没生效。使用ssh-copy-id的时候,如果目标目录或文件不存在,工具会自动创建,权限通常是合适的。但如果是手动追加,比如从别的地方复制公钥过来,就要注意.ssh目录本身是否存在。不存在的话,先建目录再写入文件,同时把权限设置好:
mkdir -p /home/username/.ssh chmod 700 /home/username/.ssh touch /home/username/.ssh/authorized_keys chmod 600 /home/username/.ssh/authorized_keys echo "公钥内容" >> /home/username/.ssh/authorized_keys再比如客户端指定了错误的私钥。如果~/.ssh/目录下存在多个密钥对,SSH会按默认顺序尝试;但如果你用的是自定义名称的密钥,或者通过SSH config指定了某个KeyFile,而这个KeyFile并不是对应该服务器的那对密钥,服务端验证签名时就会失败。排查时可以显示指定私钥进行连接:
ssh -i /path/to/private_key -o PreferredAuthentications=publickey user@host如果加上了-i参数后免密成功,说明是客户端默认密钥选择的问题。这时把正确私钥路径写入~/.ssh/config即可。
还有一个更隐蔽的场景:私钥设置了passphrase。SSH在加载私钥时会要求输入保护密码,某些自动化脚本因为没有交互终端,就会卡在“验证私钥密码”这一步。如果私钥设置了passphrase,需要用ssh-agent配合ssh-add来解锁,或者为自动化场景单独生成一把没有passphrase的专用密钥,避免调度任务卡住。
6. 长期维护中我养成的一些习惯
6.1 用注释管理多台服务器公钥
在authorized_keys文件里,每行公钥后面其实可以追加一段注释文本,用空格隔开即可。比如:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE... deploy-user@build-server这个注释会跟随公钥一起保存在文件里。建议把用途、来源、负责人信息写清楚,比如ci-deploy from jenkins、backup from backup-server。整套排查工作通常都是在故障时进行的,注释清晰的authorized_keys能让你一眼看出每条公钥是给谁用的,也不会在需要清理时误删重要密钥。
6.2 优先使用ed25519而不是rsa
新生成密钥时,我比较推荐用ed25519算法,而不是传统RSA。ed25519的密钥更短、生成速度更快、安全性也更高,而且现代OpenSSH都支持。生成命令:
ssh-keygen -t ed25519 -C "description" -f ~/.ssh/host_key如果是老系统,可能只支持RSA,那就继续用RSA。但新环境尽量直接用ed25519,少留历史包袱。
6.3 定期清理和审计
known_hosts文件时间久了会积累大量“僵尸记录”,比如已销毁的服务器、换过IP的主机、曾经用于测试的临时容器。建议每隔几个月检查一次,用ssh-keygen -F逐个确认还有哪些主机在使用,不用的直接清理。authorized_keys文件也一样,定期核对公钥是否仍然需要,离职或项目下线后及时移除对应公钥。
在生产环境,可以结合配置管理工具统一管理authorized_keys文件,把各服务器的公钥分发集中到一个配置源。好处是权限、属主、内容都能做到标准化,不会出现某台服务器手写了一个奇奇怪怪的权限导致连接不上。
6.4 小技巧:用ssh -vvv培养排查直觉
最后分享一个我自己的习惯。遇到SSH问题时,我第一反应不是翻文档,而是先在客户端执行一遍ssh -vvv连接目标主机。输出内容虽然多,但关键信息都很集中:客户端会告诉你它尝试了哪些认证方法、加载了哪个私钥、服务端的PubkeyAuthentication是否打开、服务端从哪里读取authorized_keys。多读几次之后,你会对“正常流程长什么样”形成肌肉记忆,以后再遇到异常,光看输出第几行就能猜到大概问题。
SSH免密登录说白了就是一个“链路上所有环节都对了才会成功”的事情。日志、权限、指纹、SELinux,每个点都不难,但组合在一起时很容易让人摸不着头脑。按照“先看日志再多验证”的方式,一步步排除,大多数问题都能在十分钟内定位。希望这篇内容能帮你省下一些排查时间。