1. 从一次诡异的“免密登录失效”说起
前阵子同事突然在群里喊我,说他的Git推送一直报Permission denied (publickey),而昨天还好好的。他第一反应是“我的SSH密钥是不是过期了?”——这几乎是所有人遇到SSH认证失败时的第一反应。但说实话,SSH密钥本身在绝大多数情况下并不会“自动过期”,真正让你反复撞墙的往往是另一套完全不同的机制在起作用。
先说结论:SSH密钥是否过期,取决于你用的是哪种“密钥”。
- 如果你用的是传统的
~/.ssh/id_rsa或id_ed25519这种非对称密钥对,那除非你自己手动删除或者服务端把公钥从authorized_keys里清掉,否则它天然没有“有效期”的概念,几十年后依然能用。 - 如果你用的是 SSH CA 签发的证书(
ssh-keygen -s生成的id_xxx-cert.pub),那它就有明确的有效期,默认是validity参数控制,过期后立刻失效。 - 还有第三种更常见的情况:你的私钥其实没坏,公钥也在服务端,但服务端改了配置、你把
~/.ssh目录权限搞乱了、或者ssh-agent里的密钥被清空了,结果表现和“密钥过期”一模一样。
这篇文章就把“SSH密钥过期”这件事从头到尾掰开揉碎讲清楚。不管你是被Permission denied折磨的开发者,还是管着一堆Linux服务器、时不时要给同事配密钥访问的运维,都能从这里找到可操作的排查路径和解决思路。后面还会聊到如何主动给密钥设置有效期、用CA统一管理密钥生命周期,这些属于进阶内容,但实际用起来比想象中简单。
2. SSH密钥到底会不会“过期”
2.1 传统密钥本来就不过期,除非你踩了这三个坑
很多人的所谓“密钥过期”,基本都发生在下面这三个场景里。我按出现频率排个序,你可以对照自己的情况看。
第一个坑:服务端的公钥被移除了。你在服务器上~/.ssh/authorized_keys里放的那串公钥被别人删了,或者服务器重置过、换过用户、迁移过系统,authorized_keys文件没了。这种时候你本地私钥就算长出花来也没用,因为服务端不认你的公钥。判断方法很简单,用ssh -v看输出,如果服务端直接返回No more authentication methods to try,基本就是公钥没被加载。
第二个坑:本地权限不对,或者私钥根本没被读取。OpenSSH 对~/.ssh目录和里面文件的权限有严格要求:私钥文件不能是其他用户可读的,否则客户端会直接忽略它。我见过不少人在Windows上用编辑器把id_rsa的权限改乱了,或者在Linux上从别处拷贝密钥时没注意属主和权限,结果一登录就报bad permissions。这种问题表现起来也像“密钥过期”,因为你明明看到本地有密钥文件,可就是不生效。
第三个坑:ssh-agent 里的密钥过期被清空了。如果你平时是先把私钥ssh-add加到 agent 里,然后通过代理转发(ForwardAgent)去登录跳板机再跳转目标机器,一旦agent里的条目超时或被清空,后续所有依赖转发的登录全部失败。Git平台(GitHub、GitLab等)也会定期要求你更新部署密钥,公钥还在,但平台端已禁用,一样报“密钥认证失败”。
2.2 SSH证书才是真正有“有效期”的机制
如果你用的是企业级SSH证书方案,那“过期”就再正常不过了。SSH CA 签发的证书里含有一个有效期字段,比如:
ssh-keygen -s ca_key -I user_identity -n username -V +52w user_public_key.pub-V +52w表示从现在起有效52周(也就是一年)。证书过期之后,即便你的私钥完好无损,服务端配合TrustedUserCAKeys验证证书时也会直接拒绝连接,报错内容通常是证书非法或已过期。
你可能会问:为什么已经有传统密钥了,还要搞一套CA证书?因为在大规模服务器环境里,管理员没法做到每台机器都手动管理authorized_keys。用CA统一签发、统一吊销证书,管理员只需要在服务器上配置信任一个CA公钥,所有用户的访问控制就能集中管控。这是很多中大型公司内部常用的方案。
2.3 服务端算法策略变化导致的“假过期”
还有一种隐蔽情况:服务端的SSH策略变更,不是你的密钥过期了,而是它不再接受你用的算法。
比如某些较新版本的 OpenSSH 默认禁用了ssh-rsa(SHA-1签名)这种老算法。如果你还在用老的ssh-rsa公钥,连接时服务端会直接说不支持,表现就像密钥失效一样。Git平台因为安全升级淘汰旧算法时,也会在文档里公告,建议用户生成新的ed25519密钥重新配置。
3. 排查“密钥过期”的标准姿势
3.1 三步快速定位问题出在哪一端
遇到SSH登录失败或密钥失效,别慌也别急着重新生成密钥,先按下面三步做,基本能定位九成的问题。
第一步:增加SSH日志输出,看客户端到底干了什么。
ssh -vvv user@target_host注意看输出里这些关键信息:
Offering public key: /home/you/.ssh/id_ed25519—— 客户端是否尝试提供了你的私钥。Authentications that can continue: publickey—— 服务端是否允许公钥认证。send_pubkey_test—— 客户端提交公钥后,服务端的响应如何。
第二步:检查服务端的验证日志。
登录到服务器(如果还能用其他方式登录,比如通过管理控制台、VNC等离线操作),查看认证日志:
sudo grep sshd /var/log/auth.log | tail -20 # 或者 CentOS/RHEL 上 sudo journalctl -u sshd --since "10 minutes ago"如果看到Failed publickey for user ... from ...,说明服务端收到了公钥,但没在authorized_keys里找到匹配项,或者密钥算法不匹配。
第三步:确认本地密钥文件和配置文件的权限。
ls -l ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 ~/.ssh/id_rsa 2>/dev/null chmod 644 ~/.ssh/*.pub同时确认一下~/.ssh/config里是否有奇怪的IdentityFile指定,比如指向了错误的私钥路径,或者IdentitiesOnly no之类的设置影响了密钥顺序。
3.2 判断是“真过期”还是“假失效”的一张速查表
| 现象 | 可能原因 | 初步判断方法 |
|---|---|---|
客户端显示Permission denied (publickey),但日志里没有公钥尝试 | 本地权限错误或没找到可用密钥 | ssh -vvv看是否出现Offering public key |
服务端日志提示Failed publickey | 公钥不在authorized_keys中 | 在服务端手动grep公钥内容确认 |
| Git平台提示“已用旧密钥” | 平台端公钥被禁用或删除 | 登录平台检查密钥列表与最后使用时间 |
连接时提示remote host identification has changed | known_hosts里旧主机密钥不匹配 | 确认服务器IP是否变更,系统是否重装 |
| 跳板机登录后二级跳转失败 | ssh-agent没开启或密钥未添加 | 本地执行ssh-add -l确认agent列表 |
证书方式连接提示Certificate invalid | SSH证书超过有效期 | 用ssh-keygen -L -f查看证书有效期 |
老算法报no matching key exchange method | 服务端禁用旧算法 | 服务端查看sshd_config的KexAlgorithms |
这里我特别想强调一点:known_hosts报错经常被误判成“密钥过期”。它其实是主机的密钥指纹变了,多数是服务器重装系统或IP被重新分配导致的。处理方式不是换掉你的登录密钥,而是删除旧的主机密钥记录:
ssh-keygen -R target_host再重新连接即可。可别手贱把~/.ssh/known_hosts整个删了,虽然也能解决,但会把自己机器上所有主机的指纹记录都清掉。
3.3 最容易忽略的sshd_config排查点
如果客户端和服务端日志都没有明确报错,但密钥认证就是不工作,请检查服务端sshd_config里这几个参数:
sudo sshd -T | grep -E "pubkey|authorizedkeys|passwordauthentication|usepam"重点关注:
PubkeyAuthentication yes—— 必须开启公钥认证。AuthorizedKeysFile .ssh/authorized_keys—— 确认查找的路径。PasswordAuthentication no—— 如果关了密码登录,而公钥又不生效,那就彻底进不去了,这就是很多运维后怕的“把自己锁在门外”场景。- 如果启用了
UsePAM yes,需要注意PAM模块配置也可能拦截公钥登录,尤其在CentOS上很常见。
另外,如果你改了sshd_config,记得重启服务前先做个语法检查:
sudo sshd -t配置有误时不要关掉当前连接,直接重启很容易把自己踢下线。稳妥的做法是先保持一个已登录的连接不退出,然后用sudo systemctl reload sshd平滑重载。
4. 实际修复:从本地到服务端的全套操作
4.1 重新生成密钥并配置免密登录
如果排查到最后你确实想换一把新密钥(比如旧私钥泄露过、算法太旧、Git平台要求更换),那就按这个流程来,干净利落。
第一步,生成新的密钥对:
ssh-keygen -t ed25519 -a 100 -C "your_email_or_comment"推荐用ed25519,不要去生成DSA或1024位的RSA了,早就不安全了。-a 100是设置KDF迭代次数,增加破解难度,实际影响很小,但属于好习惯。
第二步,把公钥拷到服务器并加入authorized_keys:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@target_host如果没有ssh-copy-id,手动追加也可以:
cat ~/.ssh/id_ed25519.pub | ssh user@target_host "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"注意先把新公钥追加进去、确认新密钥能登录成功之后,再考虑移除旧的,不要切断自己的后路。
第三步,测试新密钥:
ssh -i ~/.ssh/id_ed25519 user@target_host如果平时是依赖~/.ssh/config来管理多个主机,记得把IdentityFile参数改成新路径。改动之后可以用ssh -G target_host来查看最终生效的连接参数,这个命令能帮你看到所有继承和默认配置。
4.2 常见平台场景:Git、VSCode远程、群晖NAS
结合大家在热词里提到的场景,我挑几个高频的单独说。
Git平台密钥过期或更换。
不管是GitHub、GitLab还是码云,流程都差不多:本地生成新密钥 -> 把公钥粘到平台后台 -> 本地用ssh -T git@github.com验证。注意Git平台用的是git用户,不要写错成自己名字。如果你配置了多个Git账号,记得在~/.ssh/config里按Host区分不同的密钥:
Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_github IdentitiesOnly yesIdentitiesOnly yes很关键,它强制SSH客户端只使用你指定的密钥文件,而不是把agent里的所有密钥挨个试一遍。否则当你同时持有多个私钥时,Git平台会因为“该密钥已被使用”而拒绝,表现也像密钥失效。
VSCode连接SSH远程服务器失败。
VSCode Remote-SSH 本质是调用本地的ssh命令,所以排查方法和终端SSH一样。在VSCode里打开输出面板,选择Remote - SSH通道,能看到详细的连接日志。常见问题包括:
- VSCode的服务端安装脚本下载失败,表现像连上了但打不开窗口。
- 远端
~/.vscode-server目录权限混乱,导致扩展无法同步。
如果服务端更新过系统或清理过目录,重装~/.vscode-server是最快的:
rm -rf ~/.vscode-server再重新连接,VSCode会自己重建环境。
群晖上配置SSH密钥。
群晖的DSM系统本质也是Linux。开启SSH功能后,管理员用户的home目录默认可能未启用,需要先在控制面板的“用户与群组”里启用用户主目录,否则~/.ssh/authorized_keys没法正常写。另外一个坑是群晖的sshd_config对公钥认证默认开着的,但部分型号或系统版本会把 SFTP 根目录限定在用户主目录内,此时如果密钥没问题还连不上,就要检查是否被 home 目录限制挡住了。
4.3 无密码登录失效后的保底方案
不管你是运维还是自己折腾服务器,最怕的就是密钥失效时,服务器又把密码登录关了,导致彻底进不去。这种时候如果云厂商控制台提供VNC或网页终端,可以从那里登录服务器,修正sshd_config或用passwd重置账户。
如果连VNC都进不去,那就只能通过云厂商提供的“重置密码”功能进入单用户模式(或恢复模式)重置密码。经历过一次之后,我强烈建议你养成几个习惯:
- 配置新的密钥并验证成功之前,不要删旧的。
- 不要把
PasswordAuthentication立刻设为no,先切换阶段观察几天。 - 定期检查服务器上的
authorized_keys,看有没有异常公钥混进来。
5. 更主动的做法:给密钥加上生命周期管理
5.1 用 SSH CA 对用户证书做有效期限定
回到文章标题,“你的SSH密钥可能已经过期了”——如果要让这句话从“可能”变成“一定会”,那就得引入CA签发的证书机制。它在大型环境里的价值在于:你能人为规定每个用户密钥的有效期,到点自动失效,无需登录每台服务器去删除公钥。
搭建一个简化的流程大概是这样的:
第一步,创建CA根密钥。
ssh-keygen -t ed25519 -f ca_user_ed25519 -C "SSH CA for user auth"CA根密钥必须妥善保管,它就相当于你整个SSH体系的“万能钥匙”,泄露了后果不堪设想。建议放到离线环境或用硬件密钥管理设备保存。
第二步,把CA公钥配置到目标服务器。
在服务器的sshd_config里加入:
TrustedUserCAKeys /etc/ssh/ca_user_ed25519.pub然后把ca_user_ed25519.pub放到该路径,重启sshd。此后所有持有该CA签发的证书的用户,都可以登录服务器。
第三步,为用户签发带有效期的证书。
ssh-keygen -s ca_user_ed25519 -I "user1" -n remoteuser -V -1d:+30d ~/.ssh/user1.pub-n remoteuser指明证书允许用来登录哪些账户,-V -1d:+30d表示证书有效期为昨天到未来30天——这样能避免因为客户端与服务器时间差导致证书无效。签发后把user1-cert.pub文件拿给用户,放到他本地的私钥旁边,OpenSSH会自动加载同名前缀的证书。
第四步,验证证书信息。
ssh-keygen -L -f ~/.ssh/user1-cert.pub能看到证书的有效期、允许登录的账户列表、签发人等详细信息。
5.2authorized_keys里的expiry-time选项
除了CA证书,OpenSSH 的authorized_keys文件里还支持expiry-time选项,直接在公钥条目里限定有效期:
expiry-time="20260401",command="/usr/bin/true",restrict ssh-ed25519 AAAAC...到了指定时间,这条公钥自动失效。这个功能很适合给临时合作方开访问权限:你约定一个统一的过期时间,到期后不用专门去删公钥,系统自动拒绝。虽然它的灵活性不如CA证书,但在小规模场景下足够用了,也更轻量。
5.3 密钥轮换和监控脚本
不管用不用CA,建议都做一套密钥轮换和监控的机制。现在的做法可以很简单:
- 写一个脚本定期扫描所有服务器的
authorized_keys,检查是否有超过180天没有被使用的公钥记录。 - 对重要系统开启邮件告警,一旦有新的公钥加进来,立刻通知相关管理员。
- 把
~/.ssh/authorized_keys用户的权限检查加入巡检系统,防止出现其他人可读的情况。
下面给一个简单的巡检示例脚本(Python):
#!/usr/bin/env python3 import os import stat import sys def check_authorized_keys(path): if not os.path.exists(path): print(f"[WARN] {path} does not exist") return 1 st = os.stat(path) if stat.S_IMODE(st.st_mode) & 0o022: print(f"[WARN] {path} has group/other writable bits: {oct(st.st_mode)}") return 2 print(f"[OK] {path}") return 0 if __name__ == "__main__": ret = 0 for p in sys.argv[1:]: ret |= check_authorized_keys(p) sys.exit(ret)脚本的执行结果可以接入监控告警,在权限出现问题时就触发通知。实际管理规模大了之后,密钥这东西最怕的其实不是算法被破解,而是权限没人管、公钥随便进、过期没人清。把检查自动化以后,你就不必等到“密钥过期导致线上事故”的那一天再焦虑了。
6. 常见问题速查:遇到这些报错可以直接抄答案
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
Permission denied (publickey) | 公钥不被服务端接受 | 检查authorized_keys、是否指定了正确的IdentityFile、ssh-add -l确认agent |
Load key "/home/you/.ssh/id_rsa": bad permissions | 私钥权限过宽 | chmod 600 ~/.ssh/id_rsa |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED | 服务器主机密钥改变 | ssh-keygen -R target_host,确认无攻击风险后再连接 |
Agent admitted failure to sign using the key | agent 无法使用该密钥签名 | ssh-add ~/.ssh/id_ed25519重新加入agent |
no matching key exchange method found | 客户端和服务端算法不匹配 | 在~/.ssh/config临时指定兼容算法,或升级客户端 |
Connection closed by remote host | 可能是sshd配置异常、TCP封装拒绝 | 查看服务端认证日志,确认AllowUsers、AllowGroups等设置 |
Certificate invalid: expired | SSH证书过期 | 用ssh-keygen -s重新签发,缩短或适配有效期 |
这里再分享一个我自己踩过的坑:有次排查某个集群“密钥过期”,查了一整天,最后发现是服务器的/tmp目录写满了。OpenSSH登录时如果需要在/tmp下创建临时文件(比如U2F设备配合时),就会静默失败,表现就是公钥认证不通过。清掉临时文件后,一切恢复正常。平时排查的时候别只盯着密钥本身,系统资源、防火墙、SELinux、TCP Wrapper这些外围因素也要纳入考虑范围。
7. 给不同角色的一点心得
如果你只是个人开发者,平时连几台自己的服务器,最重要的习惯就是:不要在多台机器之间随便拷贝私钥,每台机器尽量用自己的密钥对;真要拷贝,也把私钥放在加密容器或密钥管理工具里。如果你用Windows,建议在 WSL 或 PowerShell 的 OpenSSH 客户端中操作,文件权限问题会少很多。
如果你是企业里管服务器的人,那 SSH密钥管理这件事迟早要从“手工维护”走向“半自动化”,搞个 CA 签发、加上有效期限定、配合巡检和审计,能省掉绝大多数和“密钥过期”相关的半夜告警。现在云厂商也都提供SSH密钥对管理能力,用起来比传统方式方便,但底层原理还是那套:公钥留在机器上,私钥保存在本地,过期与否取决于你如何定义信任边界。
最后,如果你正在被某个“密钥过期”问题困扰,建议先冷静下来,用ssh -vvv把日志看一遍,再对照上面的速查表走一遍。大多数时候你不需要重新生成密钥,问题出在权限、配置和文件路径上。真到了需要换密钥的时候,记得先让它能工作,再去处理旧的,一步步来,就不会把自己锁在门外。