SSH密钥“过期”真相:失效场景定位与修复实战

SSH密钥“过期”真相:失效场景定位与修复实战 周一早上第一件事就是发现手头三台测试服务器全部连不上了SSH客户端清一色提示Permission denied (publickey)。当时我的第一反应和大多数人一样完了我的SSH密钥是不是过期了围着这个问题折腾了一上午之后我才明白所谓“SSH密钥过期”在真实工作中根本不是一个单一问题它背后藏着七八种完全不同的故障。这篇内容就是想跟所有靠SSH吃饭的运维和开发朋友聊聊这件事SSH密钥到底会不会过期、各种“看似过期”的失效场景分别怎么定位、怎么修复以及怎么把这套东西变成长期不用操心的状态。如果你也遇到过密钥忽然失效、git推送失败、远程服务器连不上的情况这篇文章应该能直接帮你节省一个上午的排查时间。1. SSH密钥“过期”的真相失效场景远比你想象的多1.1 先搞明白一件事SSH密钥本身有“保质期”吗标准OpenSSH生成的普通密钥文件比如用ssh-keygen -t ed25519生成的私钥和公钥本质上就是一对非对称加密的字符串只要文件不被删除、不被覆盖、密码不遗忘它在数学层面是不会自然过期的。我见过有人把十年前生成的RSA密钥拿出来用照样能登录。所以严格来说SSH密钥没有“保质期”这个概念。但问题在于实际工作中“密钥失效”的感受却是真实存在的。拿我早上的故障举例最后查出来根本不是密钥文件坏了而是服务端更新了一批配置把我常用的那台机器从允许公钥登录改成了只允许特定用户组登录。这种行为在用户侧的体验就是昨天还好好的今天怎么都连不上第一反应当然是“密钥过期了”。所以我认为把“SSH密钥过期”理解成一个现象描述比理解成一个技术定义更准确。它包含三类完全不同的情况真正的物理过期使用SSH证书Certificate认证时证书里带有有效期Validity到期后认证必然失败。逻辑层面的失效公钥文件被动过、授权列表被调整、账号被限制、权限配置错误导致SSH服务端直接忽略了你的公钥。用户感知层面的“假过期”known_hosts主机密钥发生变化、网络超时、会话挂起、SSH Agent没有加载密钥、客户端配置指向了错误文件这些情况会让用户误以为是自己的密钥坏了。搞清楚这三种类型之后排查方向就清晰了。下面我会沿着“从现象到根因”这条线把每一个场景展开讲。1.2 SSH密钥认证的工作原理一把钥匙配一把锁要理解为什么“密钥没坏却登录失败”需要先简单过一遍SSH公钥认证的流程。整个过程其实很像门禁系统你的私钥相当于钥匙它保存在你本机比如~/.ssh/id_ed25519绝对不能泄露。你的公钥相当于锁芯的备案信息它被管理员放到服务器上~/.ssh/authorized_keys文件里。当你发起SSH连接时客户端会向服务器出示自己支持的认证方法然后说“我有私钥”服务器就会用一个随机数挑战来验证你是否真正持有对应私钥。如果验证通过就放行。关键点在于服务器只认它自己那份authorized_keys文件里的公钥。哪怕你本地的私钥完好无损只要服务器端的公钥记录被删除、被覆盖、被错误配置路由到了别的文件你都会被拒绝。而且服务器在拒绝时返回给客户端的错误往往相当统一都是Permission denied (publickey)并不会告诉你“你的公钥不在我的授权清单里”。这就是为什么这个故障这么容易让人误判。1.3 哪些场景最容易让人把“失效”怪罪到密钥头上根据我自己这些年接触过的案例最容易被误判为“SSH密钥过期”的场景集中在这几类Git托管平台的密钥变更GitHub、GitLab、Gerrit这些平台在添加新公钥之后如果你本机换了私钥或者平台侧删掉了旧公钥第一次push就会收到Permission denied (publickey)。很多人这时候完全想不起来自己换过电脑、清理过平台设置。服务器重装或迁移重装系统会生成新的主机密钥原有的known_hosts记录对不上客户端会提示Host key verification failed这个报错看起来跟“密钥”相关实际是主机密钥的问题跟你的登录密钥没关系。服务器端授权文件被调整管理员清理账号、轮换全局密钥、收紧登录策略比如只允许wheel组登录、禁止root远程登录这些操作都会导致原密钥瞬间失效。本地密钥文件被误改或丢失比如用过某些清理工具后~/.ssh下文件被移动、权限被改或者你在Windows上更换了用户目录导致SSH找不到原来的私钥。SSH Agent状态异常你的私钥设置了密码passphrase平时靠ssh-agent记住密码。重启电脑之后Agent里什么私钥都没有如果你又在SSH配置里特别指定了某个IdentityFile就会得到各种奇怪的连接失败。遇到这五类情况第一反应都不该是“重新生成密钥”而是先做一次系统性的定位。下一节我会给出具体的排查方法。2. 从报错到结论5分钟定位问题根因2.1 先读报错不同提示代表完全不同的故障层级定位SSH问题的基本功是能一眼分清客户端报错属于哪个层级。我常跟团队里的人说不要看到“key”“authentication”就以为是密钥问题先看看报错到底在讲什么Permission denied (publickey)认证阶段被拒。服务器告诉客户端“我尝试了公钥认证但没通过”。这里可能的原因很多包括公钥不在authorized_keys、服务器禁止公钥认证、账号被禁用等。Host key verification failed安全校验阶段失败。发生在SSH连接早期客户端发现服务器的主机密钥跟自己known_hosts里保存的不一致直接中断了连接。这跟你的用户密钥一点关系都没有。Connection timed out或Connection refused网络层问题。要么是防火墙拦了22端口要么是服务没起来要么是IP/端口写错。这种问题更谈不上密钥过期。Load key xxx: incorrect passphrase supplied to decrypt private key本地私钥密码错误或私钥损坏属于本地文件问题。很多人一看到Permission denied (publickey)就急着去重新生成密钥这是最大的误区。先花半分钟把报错归一下类排查范围能缩小一大半。2.2 用verbose模式把故障阶段拆开看SSH客户端其实内置了一个非常好的诊断工具就是-v参数。我排查问题时习惯直接用三重verbosessh -vvv useryour-server执行之后客户端会打印出整个SSH握手的详细过程。你不用全看懂重点抓这几行信息debug1: Connecting to your-server [IP] port 22.确认网络层是否能连到目标端口。debug1: Server host key: ssh-ed25519 SHA256:xxx确认服务器主机密钥已经拿到如果在前面出现Host key verification failed就说明known_hosts校验没通过。debug1: Offering public key: /home/you/.ssh/id_ed25519客户端正在尝试用某个私钥文件发起认证这里能看出客户端用了哪一个私钥文件。很多时候这里就会发现客户端加载的跟他以为的根本不是同一个文件。debug1: Server accepts key: /home/you/.ssh/id_ed25519如果服务器接受了某个密钥说明认证成功。debug1: Authentications that can continue: publickey这行非常关键它表示服务器当前允许的认证方式。如果这一行里只有password没有publickey那说明服务器端压根就没开公钥认证或者被策略限制住了你再怎么换密钥都没用。这几种状态基本能把故障定位到具体环节。如果你看到的现象是“offering之后直接permission denied”那就多半是authorized_keys或者账号权限的问题如果连offering都没有那问题出在客户端配置或Agent上。2.3 服务端日志一锤定音的证据客户端信息只能帮你缩小范围真正一锤定音要看服务端的SSH日志。在多数Linux系统上日志路径是Debian/Ubuntu系列/var/log/auth.logCentOS/RHEL系列/var/log/secure用grep过滤一下SSHD的日志能直接看到认证结果# Ubuntu / Debian grep sshd /var/log/auth.log | tail -50 # CentOS / RHEL grep sshd /var/log/secure | tail -50重点关注这几类行Accepted publickey for admin from 192.168.1.10 port 52341 ssh2: ED25519 SHA256:xxx说明认证成功登录被接受。Failed publickey for admin from 192.168.1.10 port 52341 ssh2: ED25519 SHA256:xxx说明客户端发来了公钥但服务端在authorized_keys里没找到匹配项或者公钥格式不被认可。这是最典型的“授权记录缺失”。Connection reset by 192.168.1.10 port 52341 [preauth]说明SSHD在认证完成之前连接就被中断。可能是客户端中断也可能是fail2ban之类的防护工具把你的IP临时封了。User admin from 192.168.1.10 not allowed because not listed in AllowUsers这是账户被拒绝的典型日志说明不是密钥问题是账户策略问题。我自己排查时只要client端verbose显示“Offering public key”但服务端日志出现“Failed publickey”就直接开始查authorized_keys文件内容了不用再纠结其他任何东西。2.4 标准检查顺序从外到内别跳步为了不让自己在排查时东一下西一下我给自己定了一套固定顺序也推荐你按这个顺序走网络连通性ping或nc -vz host 22先确认端口通不通。主机密钥校验看有没有Host key verification failed。认证方式协商用verbose观察服务器支持哪些认证方式。客户端密钥文件确认加载的是哪个私钥私钥权限是否正确。服务端授权文件检查 authorized_keys 内容、路径、权限以及SSHD配置中的用户策略。服务端日志用日志验证猜测。这套顺序走下来90%的“密钥过期”问题在10分钟内都能定位。下面我按修复场景展开讲。3. 修复方案不同“过期”场景的完整处理流程3.1 Git/代码托管平台的SSH密钥失效处理先说大家最常遇到的场景往GitHub、GitLab、Gitee或者Gerrit上push代码时忽然报Permission denied (publickey)。这种场景的处理流程非常固定。第一步先确认自己本机当前的公钥内容cat ~/.ssh/id_ed25519.pub如果没有这个文件那就直接生成一个新密钥。强烈建议用ed25519算法同时加高KDF迭代次数提高本地私钥的抗爆破能力ssh-keygen -t ed25519 -a 100 -C your_emailexample.com生成之后用cat ~/.ssh/id_ed25519.pub把公钥内容复制出来登录Git平台进入Settings → SSH Keys/GPG Keys页面添加新的公钥。添加完成后用测试命令验证# GitHub ssh -T gitgithub.com # GitLab ssh -T gitgitlab.com # 如果公司自建GitLab/Gerrit换成对应域名 ssh -T gitgitlab.company.com如果显示类似Hi username! Youve successfully authenticated说明密钥已经生效。需要注意的是很多公司自建Git平台同一把公钥可以反复添加但不同平台之间公钥是独立的换平台必须重新添加。还有一个常见坑很多人本机同时有多个Git平台的密钥或者公司内部平台与GitHub密钥混在一起SSH会尝试所有key直到被接受。为了明确指定某个平台用哪个私钥我建议在~/.ssh/config里写清楚Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_github Host gitlab.company.com HostName gitlab.company.com User git IdentityFile ~/.ssh/id_ed25519_company这样每次连接都会优先尝试对应私钥不会因为key太多导致平台侧匹配到已失效的公钥。3.2 服务器端authorized_keys变动导致的登录失败登录自己的Linux服务器失败时优先怀疑服务器端授权文件被改动。在能通过其他渠道比如VNC、物理控制台、或另一个仍然有效的账号登录服务器的情况下直接检查以下内容# 1. 查看当前用户家目录下的授权文件 cat ~/.ssh/authorized_keys # 2. 确认文件权限是否正确必须只有属主可读写 ls -l ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys chmod 700 ~/.ssh权限问题是我见过频率最高的一个坑。SSHD默认启用了严格的权限检查如果~/.ssh/authorized_keys对其他用户可写SSHD会直接拒绝使用这个文件并记录“bad permissions”日志。很多人复制公钥的时候用了覆盖后面又用chmod 777调整过目录结果密钥配置怎么都对就是连不上多半就是这个问题。另外还要确认SSHD实际使用的是哪个授权文件。默认路径是~/.ssh/authorized_keys但有些系统或安全加固方案会改成自定义路径甚至用脚本动态获取公钥。建议看一眼服务端配置grep -E ^AuthorizedKeysFile|^PubkeyAuthentication|^PasswordAuthentication /etc/ssh/sshd_config看到AuthorizedKeysFile /etc/ssh/authorized_keys/%u这种配置时说明系统根本不看家目录下的authorized_keys而是去集中管理目录找公钥。这种情况你需要把公钥放到对应目录下或者请管理员帮你操作。还有单位会通过LDAP或AD集中下发公钥这种场景下即使你把公钥写到本地authorized_keys也不一定生效因为配置里有AuthorizedKeysCommand选项会把查询转发给外部系统需要走统一入口更新。3.3 known_hosts冲突与主机密钥变更另一种特别容易误导人的情况是服务器重装系统或重建容器后原来的IP地址还在但主机密钥已经变了。客户端连接时会提示WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! Host key verification failed.这句提示已经很直白了远端主机密钥和known_hosts里保存的不一致。解决办法也很简单把旧记录删除ssh-keygen -R your-server-ip再重新连接客户端会提示确认新的主机密钥指纹确认后写入known_hosts即可。顺便提醒一句在收到这种警告时如果服务端不是被你重装的而是别人改的一定要先确认是不是中间人攻击。正常情况应该是你或管理员主动更换了主机密钥如果你完全不知情先联系服务端管理人员复核不要急着清掉known_hosts。对于经常重建的测试环境可以在连接时临时关闭known_hosts校验但不建议全局关闭。合理方式是在~/.ssh/config里对指定主机设置Host test-bastion HostName 10.0.0.5 StrictHostKeyChecking accept-newaccept-new表示只接受新主机的密钥指纹但如果已有记录且不一致仍然会报错。这样兼顾便利性和安全性。3.4 证书型SSH密钥的续期与重签如果你所在的企业或团队使用了SSH CA证书认证比如通过Certificate Authority批量签发用户证书那么密钥真的会到期。这种证书里的有效期是写死的到期后客户端证书不被服务器接受表现就是一把看起来完好的私钥突然之间谁都登不上了。判断你是不是在用证书型密钥很简单看本机密钥目录下有没有*-cert.pub文件ls ~/.ssh/ # 如果看到 id_ed25519-cert.pub 之类的文件说明用的就是证书认证可以用以下命令查看证书的有效期ssh-keygen -Lf ~/.ssh/id_ed25519-cert.pub输出里会有一段Type: ssh-ed25519-cert-v01openssh.com user key Valid: from 2024-01-01T00:00:00 to 2025-01-01T00:00:00Valid的结束时间就是证书的到期时间。到期后你需要找CA管理员重新签发或者如果自己持有CA私钥可以重新签发ssh-keygen -s ca_key -I user_identity -n remote_username -V 52w user_key.pub这里各参数含义-s ca_key指定CA私钥-I是证书标识-n指定允许登录的用户名通常授权给哪个账号就填哪个-V 52w表示有效期从当前时间往后算52周。签发后把user_key-cert.pub放到~/.ssh目录并与私钥同名SSH会自动加载。证书型密钥的好处是集中管控和定时轮换但代价就是到期前必须有人处理。很多企业故障就是这么来的有证书回收策略但运维没建提醒机制结果全员在某个周一集体裸奔。4. 把手动修复变成自动化密钥管理的长期方案4.1 密钥文件的分层管理与配置拆分经历过几次“密钥失效”之后我最大的体会是SSH密钥不适合临时抱佛脚式管理它需要一套稳定的组织方式。我现在管理多台服务器和多个Git平台时按以下规则组织每个职责范围生成独立密钥比如id_ed25519_work、id_ed25519_home、id_ed25519_git便于失效时定点回收不用全局换Key。所有免密登录都通过~/.ssh/config定义Host别名不在命令行里手动指定-i参数。私钥目录权限设置为700私钥文件600公钥文件644。有密码的私钥统一交给ssh-agent管理登录终端时自动加载eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519_work这样做最直接的好处是当某个密钥出了问题你只需要更换那一把对应关系的Key不会因为“一把钥匙走天下”而被迫全局更新也不会因为密钥太多而搞混。如果主机数量很多我很推荐把~/.ssh/config按环境拆成多个文件再通过Include引入Include ~/.ssh/config.d/work.conf Include ~/.ssh/config.d/home.conf Include ~/.ssh/config.d/git.conf每次增加新服务器只需要改动对应的conf文件不影响其他环境回滚也方便。SSH从7.3p1开始支持Include绝大多数现代系统都没问题。4.2 批量环境下如何快速检测“即将过期”的密钥对于运维来说最怕的是密钥问题集中爆发。我建议把检测做成例行巡检别等用户反馈。针对证书型密钥可以用一条简单的Shell脚本扫描所有证书何时到期#!/bin/bash for cert in ~/.ssh/*-cert.pub; do [ -e $cert ] || continue expiry$(ssh-keygen -Lf $cert | grep Valid | sed s/.*to //) echo $cert - $expiry done对于普通密钥的批量认证测试可以直接对主机列表做一次免密探测while read host; do if ssh -o BatchModeyes -o ConnectTimeout3 -o StrictHostKeyCheckingaccept-new $host echo OK 2/dev/null; then echo $host: reachable else echo $host: FAILED fi done hosts.txt这里BatchModeyes的作用是禁止交互式输入密码如果密钥认证失败就直接报错退出不会卡在输入密码环节ConnectTimeout3避免对不可达主机长时间等待。这个脚本可以作为每周巡检跑一次谁家密钥失效一目了然。4.3 会话超时与挂起问题的预防参数再说一个容易被归因到“密钥过期”的误报场景SSH连上之后长时间不操作再回来发现终端卡死重新连接又说密码错误。这种问题通常不是密钥失效而是会话被服务端或中间设备断开了。防范方法有两个层面客户端侧在~/.ssh/config里对目标主机设置Host * ServerAliveInterval 30 ServerAliveCountMax 3ServerAliveInterval 30表示每30秒发送一个保持连接的空包ServerAliveCountMax 3表示连续3次没收到响应才判定断开。这样长时间挂机也不会被防火墙静默丢弃。服务端侧可以在/etc/ssh/sshd_config里配置ClientAliveInterval 60 ClientAliveCountMax 3含义类似作用是让服务端主动探测空闲连接。要注意的是如果服务端设了较短的ClientAliveInterval比如30秒而客户端设置不匹配用户挂机时间稍长就会被踢下线体验上很像“密钥又过期了”。建议内外配置协调好不要两边都设得过于激进。4.4 常见SSH客户端与工具链的最佳搭配我发现很多“SSH密钥过期”问题其实是客户端工具使用习惯导致的。比如你用VSCode的Remote-SSH插件连接远程服务器它读取的也是~/.ssh/config和标准的密钥文件如果之前你在VSCode里指定过某个IdentityFile换了机器后忘了同步就会反复提示无法连接。VSCode Remote-SSH排错时可以打开“Remote-SSH: Show Log”查看完整日志基本能还原整个连接流程。Windows环境下很多人用Bitvise SSH Client或Xshell这类工具默认会生成自己格式的密钥对或者把配置文件放在程序私有目录里容易跟系统OpenSSH的密钥体系脱节。我的建议是尽量统一使用标准OpenSSH格式的密钥即ssh-keygen生成的文件无论用什么客户端都能导入。Bitvise SSH Server作为服务端时配置公钥认证的路径跟OpenSSH不完全一样需要在它的管理界面里手动导入公钥如果你同时在用系统自带的OpenSSH服务两边各配各的很容易出现“同一个密钥在一台服务器上能登另一个不能登”的现象。现在很多局域网设备比如群晖NAS、华为交换机也都支持SSH密钥登录。配置逻辑大体相同公钥放到对应用户的authorized_keys或设备管理界面的指定区域私钥留在本地。区别只在于这些设备往往有自己独立的权限体系比如群晖如果开了家目录加密重启后可能无法自动挂载加密的home目录导致读取不到authorized_keys这时候也需要先从控制台确认目录状态。5. 常见问题速查与避坑记录5.1 故障速查表我把这些年遇到过的典型的“SSH密钥过期”类问题整理成了一张表排查时可以对照着看现象可能原因处理方式提示Permission denied (publickey)公钥不在服务器authorized_keys重新添加公钥确认路径和权限提示Permission denied (publickey)服务器禁止公钥认证PubkeyAuthentication no修改sshd_config并重启sshd提示Permission denied (publickey)账号被AllowUsers/AllowGroups限制检查账户策略加入白名单提示Host key verification failed服务器主机密钥变更用ssh-keygen -R删除旧记录后重连提示Connection reset by ... [preauth]IP被fail2ban等防护工具封禁解封IP或等封禁时间结束提示Load key ... incorrect passphrase本地私钥密码输入错误核对passphrase或重新生成密钥客户端一直加载错误密钥文件SSH配置或Agent加载了多余IdentityFile检查~/.ssh/config和ssh-add -l能够认证成功但马上断开用户的默认Shell无效或无权限修改用户shell或用控制台修复证书认证提示invalid or expired certificate证书型密钥有效期已过用CA重新签发或找管理员续期这张表覆盖了我日常工作中至少80%的同类问题。排查时先对号入座再结合verbose和服务端日志确认基本不会走偏。5.2 我踩过的几个坑第一个坑是authorized_keys文件权限过宽。有一次我为了让另一个同事也能用同一账号登录手动改了/home/admin目录权限改成755结果SSH直接拒绝加载authorized_keys客户端一直报publickey失败。后来看日志才发现是“Authentication refused: bad ownership or modes”把目录权限改回700authorized_keys改成600问题立刻消失。记住一句话SSH对权限的敏感度远超你想象普通文件权限“没问题”不等于SSH认为“没问题”。第二个坑跟NFS有关。公司有一部分服务器的home目录挂载在NFS存储上某次存储侧变更导致home目录读取延迟用户登录时SSHD读取不到authorized_keys报的错跟密钥不存在一模一样。这种问题很难从客户端看出端倪只能通过服务端日志和存储侧审查发现。如果你的环境里home目录是网络存储排查密钥“莫名失效”时多留个心眼。第三个坑是默认Shell问题。用户的公钥和authorized_keys都正确认证也已经通过了但连接立即断开没有任何错误提示。最后发现是用户默认Shell指定了一个不存在的路径SSH完成认证后解析Shell失败就断开了。用控制台把Shell改回/bin/bash后恢复。第四个坑是多账号混合场景下的配置打架。有人在同一台机器上有多个Git身份结果在~/.ssh/config里把不同域名指向了同一个IdentityFile导致某个平台key被另一个平台拒绝。这种问题在ssh -T gitgithub.com时往往看不出但在同时使用多个平台时就暴露了。配置拆分之后清爽很多。5.3 两个提高效率的小技巧最后分享两个我在实际使用中觉得非常顺手的小技巧。第一个是用ssh -G检查SSH最终生效的配置参数。当你怀疑客户端加载了错误配置时直接执行ssh -G hostname它会列出连接时将会使用的所有参数包括实际加载的IdentityFile、端口、用户、超时时间等能避免你自己翻配置文件脑补。尤其是使用Include拆分配置后这个命令几乎成了我的默认排错第一招。第二个是把服务器主机公钥指纹固定到known_hosts里。对于固定不变的生产服务器我建议在首次连接确认指纹无误后直接把主机公钥写入known_hosts甚至config文件里用HostKeyAlgorithms或UserKnownHostsFile指定准确值这样即使出现变更也能第一时间察觉并主动处理而不是在故障发生时被动追查。对于这种操作我的体会是SSH密钥管理没有一劳永逸的魔法但如果你把目录结构、权限、配置文件、巡检脚本都提前理顺那点小麻烦根本不值一提。至少现在我遇到“密钥失效”的第一反应已经变成先看日志而不是慌慌张张重新生成密钥了。