你的SSH密钥可能已经「过期」了搪
长假回来,第一件事就是连服务器,结果Permission denied (publickey)直接糊脸。多数人的第一反应是“密钥过期了”,然后把~/.ssh翻了个底朝天,重新生成密钥、重新上传公钥,折腾半小时问题照旧。我甚至见过有人在群里问“macOS 升级之后 SSH 密钥是不是会失效”,底下还有人说“会,重装系统必须重新生成”。
这里先泼一盆冷水:裸的 SSH 私钥文件本身,几乎不存在“有效期”的概念。用ed25519或rsa生成出来的私钥,只要文件没坏、权限没错、密码短语没忘,它十年之后还是同一把钥匙。真正会“过期”的,是整条公钥认证链路里某个环节失效了——服务端authorized_keys被清理、客户端身份文件没有被正确选用、known_hosts变化、ssh-agent缓存失效、甚至远程主机换了新算法。绝大多数“密钥过期”的报错,背后都是这类问题。
这篇文章不打算给你堆一堆概念,而是把这些年被“密钥过期”坑过的场景拆开揉碎,从排查思路到实操配置,再到 Git 平台、VSCode Remote、批量运维这类常见环境下的版本,一次性梳理清楚。不管你是刚上手 Linux 的小白,还是每天要跨几十台机器的运维,这里面的坑和对应的解法,大概率你都遇得到。
1. “密钥过期”这个说法,藏着几个容易被混淆的场景
1.1 私钥文件本身几乎不会过期,真正会失效的是“认证链路”
先理清楚 SSH 公钥认证到底是怎么工作的。你在客户端持有的~/.ssh/id_ed25519是私钥,它不会主动过期。真正决定你能不能登录的,是服务端~/.ssh/authorized_keys里是否还存着你对应的公钥。整个认证链路可以简化成四段:
- 客户端本地持有私钥;
- 连接时客户端向服务端声明“我这里有私钥,请验证”;
- 服务端在
authorized_keys里查找对应的公钥; - 找到后,服务端用一个随机挑战值让客户端用私钥签名,验证通过即放行。
这四段里任何一环出问题,最终报错都是同一个味道:Permission denied (publickey)。但服务端在拒绝时,并不会告诉你“你的公钥记录被删了”还是“你没提供任何可用的密钥”。所以排查时必须自己一步步确认。
最常见的“假过期”场景是:服务端改版或运维统一重置了authorized_keys,某个用户的公钥被清理了;或者你换了电脑、重装了系统,生成的是一对新密钥,而服务端还存着旧公钥。两边信息不对称,就产生了“密钥过期了”的错觉。
1.2 有“有效期”概念的几种特殊情况
要说“确实会过期”的场景,也不是完全没有,只是它们通常不是靠手动生成的那对裸密钥。
第一种是SSH 证书。如果你用ssh-keygen -s给用户签发过证书,那么证书本身可以携带有效期。签发时可以指定-V -1d:+30d之类的有效期范围,到期后即使authorized_keys里配置了对应的 CA 公钥,客户端持有的证书也会被拒绝。这在大型集群里很常见,管理员用 CA 集中签名,到期自动轮换。比如:
ssh-keygen -s ca_key -I user_identity -n username -V -1d:+30d user.pub这种情况下报错的字眼里往往带着Certificate invalid: expired,一眼就能看出是证书过期,和普通密钥失效不是一回事。
第二种是FIDO/U2F 硬件密钥生成的 discoverable credential。用ssh-keygen -t ed25519-sk生成的密钥,如果依赖硬件里的 PIN 或生物识别,某些场景下硬件固件升级或凭据被重置,客户端也会报无法使用密钥。这算硬件层面的“状态变动”。
第三种是ssh-agent 里的会话时效。你把带密码短语的私钥ssh-add进去了,之后一直可以免密登录。但如果重启了电脑、重启了 ssh-agent 服务,或者会话超时被清理,你会觉得“没动任何配置怎么就登录不了了”。这是 agent 的缓存没了,不是密钥失效。很多人在这个点上栽过跟头。
1.3 各种报错文案对应的真实故障类型
我自己排查过不少“密钥过期”类工单,先看报错文案,基本能判断方向。整理一张表给你,省得走弯路。
| 报错文案片段 | 更可能的真实原因 | 排查方向 |
|---|---|---|
Permission denied (publickey) | 服务端没有匹配到公钥,或客户端没提供可用身份 | 检查authorized_keys、IdentityFile选择 |
Load key ... incorrect permissions | 私钥文件权限过宽 | chmod 600 |
Host key verification failed | known_hosts里记录的主机指纹变了 | 核对指纹或移除旧记录 |
no matching key exchange method | 服务端算法太老,客户端默认禁用 | 显式指定KexAlgorithms |
Connection reset by ... port 22 | 网络中间设备干预、IP 被复用或 sshd 异常 | 抓包或看服务端日志 |
Certificate invalid: expired | SSH 证书过期 | 重新签发 |
This host key is already known且明显不匹配 | 有人篡改或换了密钥 | 联系管理员确认指纹 |
这张表不是标准答案,但能帮你把“感觉是密钥问题”和“实际是什么问题”之间拉开一点距离,后面排查就有方向了。
2. 实际案例:Ubuntu 连不上服务器,我按这个顺序排查到底
2.1 第一步:还原客户端的完整日志
别一上来就盲改配置,先看客户端日志。连接时加上-vvv参数:
ssh -vvv user@your-server输出里重点看几段:identity file、Offering public key、Authentications that can continue。举一个典型的失败片段:
debug1: identity file /home/user/.ssh/id_ed25519 type 0 debug1: Next authentication method: publickey debug1: Offering public key: /home/user/.ssh/id_ed25519 ED25519 SHA256:xxx debug1: Authentications that can continue: publickey debug1: Trying private key: /home/user/.ssh/id_rsa如果日志显示“Offering public key”之后立刻回到“Authentications that can continue”,说明服务端明确返回了“这个公钥我不认”。此时基本可以判定,问题在服务端的authorized_keys,而不是客户端。
反过来说,如果日志里压根没有“Offering public key”这一行,而是跳到“Trying private key”,说明客户端认为自己没有可用的身份文件,或者被IdentitiesOnly/IdentityFile配置限制了。先在本地把这两件事分开。
2.2 第二步:看服务端 auth.log 里对应的拒绝原因
客户端日志只能告诉你“被拒了”,具体为什么拒,要看服务端。Ubuntu 上打开:
sudo tail -n 50 /var/log/auth.log收到权限拒绝时,记录大概长这样:
sshd[12345]: Failed publickey for user from 192.168.1.10 port 54321 ssh2: ED25519 SHA256:xxx sshd[12345]: Connection closed by authenticating user 192.168.1.10 port 54321 [preauth]如果连Failed publickey这行都没有,只是纯粹的连接被重置,那就更像网络层因素。如果出现了Failed publickey,下一步就要确认authorized_keys文件里的内容到底对不对。
2.3 第三步:验证 authorized_keys 的格式和权限
这是能卡住绝大多数人的一步。检查三件事:文件权限、内容格式、路径是否正确。
# 在服务端执行 ls -la ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys常见的坑有:.ssh目录权限是755,authorized_keys是644,sshd 会直接拒绝读取;还有的同事把公钥粘贴时合并成了一行,或者中间塞了换行符,导致整条记录无效。
再确认服务端到底读的是哪个文件:
sudo sshd -T | grep authorizedkeysfile默认是.ssh/authorized_keys,但有的人改过AuthorizedKeysFile,或者是用sshd_config里的AuthorizedKeysCommand拉取动态公钥,这种情况下静态文件怎么改都没用。
2.4 一个典型的误判:客户端换了新钥匙,服务端还是旧记录
一次真实的排查:某同事换了办公电脑,把旧电脑的私钥拷了过来,公钥也跟着拷了过来,但两边authorized_keys对不上。折腾半天,最后发现旧电脑的私钥其实还在,只是他新拷的私钥是另一台机器的。更离谱的情况是,他把公钥贴到了自己的authorized_keys里,但服务端配置了AuthorizedKeysCommand从某个 CMDB 拉取,导致他自己本地的修改根本不生效。
所以排查优先顺序建议是:先确认客户端选用的是哪把钥匙(-vvv),再到服务端确认读的是哪个文件,最后才怀疑“真的过期了”。
3. VSCode Remote SSH 反复掉线:问题多半不在密钥本身上
3.1 Remote-SSH 的“身份传递”机制
很多人用 VSCode Remote-SSH 连远程开发机,某天突然提示“此扩展在此工作区中被禁用,因为其被定义为在远程扩展主机中运行”,第一反应是“我的密钥坏了”。其实这个提示说的是 VSCode 的扩展机制:你装的某个扩展只支持在远程主机上跑,不支持在当前工作区加载,和 SSH 密钥没有直接关系。它只是在你连接出问题时,被一起带出来的“次级症状”。
Remote-SSH 的工作方式,是先建立一条 SSH 连接,然后在远端下载并启动一个 VSCode Server。远程 Server 若需要访问你的内网 Git、内网平台,往往需要把本地的 SSH 身份转发到远端。如果你在 VSCode 里配置了ForwardAgent yes,或者通过ssh -A连接,远程就可以利用本机的 agent 里的密钥。一旦 agent 没生效,远程 Server 去拉私有仓库就会报权限错误,表现很像“密钥失效”。
3.2 让身份转发稳定生效的配置方式
在~/.ssh/config里给远程主机加上:
Host myserver HostName 192.168.1.100 User yourname ForwardAgent yes ServerAliveInterval 60 ServerAliveCountMax 3ForwardAgent yes开启之后,VSCode 里的集成终端如果还要访问内网 Git,也能直接使用本机的 agent,非常方便。需要强调的是,ForwardAgent是有安全边界的:远端一旦被攻破,攻击者可以用你的 agent 去访问你当前所有已加载的密钥对应的服务。如果只是跑普通开发环境问题不大,但生产跳板机建议按需关闭。
如果远端没有你的“本机 agent”,你还可以把私钥放到远端,但这一步会引入私钥拷贝的风险,我一般不建议,除非是专门的部署账号。
3.3 反复掉线的高发原因:保活、IP 复用和 HostKey 变化
Remote-SSH 更常见的“密钥过期”场景,其实是这三种。
第一种是长连接被中间设备掐断。办公网、云平台安全组都可能空闲端口超时回收,表现就是挂着挂着就断。配置ServerAliveInterval可以定期发心跳,避免被当成死连接。
第二种是IP 被复用导致 HostKey 冲突。公司 DHCP 池如果紧张,原来机器的 IP 分给了新机器,连过去会报:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@这是known_hosts里的旧指纹和新机器对不上,不是密钥失效。确认新机器没问题后,用ssh-keygen -R 目标IP删掉旧记录再连即可。
第三种是定时任务覆盖了 authorized_keys。有些公司的安全系统会定期把各服务器的authorized_keys刷新成统一状态,你手动加进去的临时公钥会被清掉。这属于“管理策略导致密钥被移除”,本质上也是授权链路断掉。遇到这种情况,别硬刚了,走正式的密钥申请流程才是正路。
3.4 如果连的是交换机或路由器这类网络设备
再补一个很容易被误会成“密钥过期”的场景:连接华为、中兴或某些老旧的交换机、路由器时,SSH 报错经常会带no matching key exchange method或no matching cipher。这不是认证失败,而是加密算法协商失败——新版本 OpenSSH 默认关闭了旧式算法,而设备固件还停留在老协议。
临时连接可以用:
ssh -oKexAlgorithms=+diffie-hellman-group14-sha1 user@switch但要注意,这类老算法存在安全风险,只能用于临时排查,有条件还是升级设备固件,或者改用带证书的现代登录方案。
4. Git 平台和批量登录场景下的密钥轮换策略
4.1 GitHub / GitLab / Gitea 上的密钥为什么会“突然失效”
不少人遇到过:昨天git push还好好的,今天直接Permission denied (publickey)。去平台后台一看,密钥列表里少了一把。
平台通常不会无故删除你的公钥,但触发条件很实在:
- 平台风控检测到某把私钥在多台机器上高频使用,判定为疑似已泄露,自动禁用;
- 你把同一把私钥复制到多台设备,某台设备的私钥文件权限不对,被监控策略扫描到;
- 公司 GitLab 的管理员定期清理长期未使用或超期未轮换的部署密钥(Deploy Key)。
更隐蔽的一种情况是:你在仓库里配置了 Deploy Key,后来仓库迁移,新的仓库没有关联原来的 Deploy Key,自动构建脚本里还带着旧密钥,于是git clone直接 403。这类问题不会在密钥本身报“过期”,但只要构建一跑就暴露。
4.2 尽早给部署用户使用专用密钥并分离权限
Git 平台上的登录密钥和部署密钥一定要分开。个人终端上用个人密钥,CI/CD 构建机上用单独的 Deploy Key,而且施加最小权限:
ssh-keygen -t ed25519 -C "ci-deploy@project-a" -f ~/.ssh/ci_project_aDeploy Key 在 GitLab/GitHub 上通常只给予某个仓库或某个项目组的只读或读写权限。只读就够了的话,绝不勾写权限。这样即使 CI 机器被侵入,攻击者能动的范围也被锁死在一个仓库里,不会拖走你整个账号下的所有仓库。
4.3 批量登录几百台机器时如何管理密钥
如果只管几台机器,手动把公钥塞到每台服务器的authorized_keys里还能忍。一旦机器到了几十上百台,这个做法就是给自己埋雷。批量场景下,我的实践优先级是:
- 跳板机集中管理:所有机器只允许通过跳板机登录,跳板机上统一管理用户公钥;
- SSH CA 签名:管理员用 CA 给用户公钥签名,服务器统一信任 CA 公钥;用户换电脑只换公钥签名,服务器端不需要任何改动;
- 定期批量替换:如果暂时做不了 CA,那就写 Ansible 或脚本批量分发
authorized_keys,但必须保留审计记录。
某个项目组曾经把公钥直接写死在机器初始化镜像里,结果有人离职后公钥还在所有服务器上,被安全扫描扫出来才紧急处理。这种事只要遇到一次,你就明白为什么分散管理是灾难。
4.4 网络设备场景下的注意点
再补一个偏冷门的:如果你管理的网络设备(华为、中兴、H3C 等)开启 SSH 后,提示密钥不匹配或者直接拒绝连接,除了前面说的算法协商问题,还可能是设备侧只支持 RSA-SHA1,而新版 OpenSSH 默认发送的是 RSA-SHA2。
在客户端加参数临时验证:
ssh -vvv -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa admin@switch如果这样能通,就要考虑设备固件升级,或者在客户端配置里给这个设备单独开PubkeyAcceptedAlgorithms +ssh-rsa。这类问题本质是“密钥算法兼容性”,不是“密钥到期”,但症状非常迷惑人。
5. 让“密钥过期”不再发生的日常配置与检查习惯
5.1 客户端的 ssh config 长什么样更不容易踩坑
我有一套维护了很久的客户端~/.ssh/config模板,能避免大量“假过期”问题:
Host * ServerAliveInterval 60 ServerAliveCountMax 3 AddKeysToAgent yes IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes Host bastion HostName bastion.example.com User ops DynamicForward 1080 Host dev-* HostName %h.dev.internal User dev ForwardAgent yes几个关键点解释一下:
IdentitiesOnly yes非常重要。默认情况下 OpenSSH 会把~/.ssh下所有私钥依次尝试一遍,如果服务端只认其中一把,但第一把尝试的钥匙返回了拒绝,有的服务端配置会直接中断整个认证流程。用IdentitiesOnly yes限制只发送你指定的身份文件,能大幅减少这种“明明钥匙没错但登录不了”的情况。AddKeysToAgent yes配合 ssh-agent,让你第一次输入密码短语后,后续连接不再反复问。如果你有多个密钥,可以再搭配Host不同主机用不同IdentityFile。
5.2 服务端怎么避免大规模密钥失联
在服务端,维护authorized_keys时注意几点:
- 每一行只放一个公钥,别把公钥和注释塞在同一行还会断行;
- 定期备份
.ssh/authorized_keys:
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak.$(date +%F)- 用
sshd -T确认实际生效的配置:
sudo sshd -T | grep -E 'pubkeyauthentication|authorizedkeysfile'- 如果要临时给某个用户添加一把期限受限的钥匙,可以考虑在后台配置 SSH CA,而不是手动追加公钥再手动删除。手动操作容易忘,遗忘的安全隐患比过期本身更麻烦。
5.3 一个简单有用的检测定时任务
真正让“密钥过期”成为可控风险的,是提前发现,而不是事后排查。下面这个脚本可以放在管理机上定时跑,批量探测目标主机的公钥登录是否正常:
#!/bin/bash # 检测批量主机 SSH 公钥登录是否正常的简单脚本 HOSTS_FILE=~/hosts_list.txt KEY_FILE=~/.ssh/id_ed25519 while read host; do [ -z "$host" ] && continue if ssh -i "$KEY_FILE" -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$host" 'echo ok' >/dev/null 2>&1; then echo "[OK] $host" else echo "[FAIL] $host" fi done < "$HOSTS_FILE"BatchMode=yes很重要,它禁止交互式输入密码,确保测试的是公钥认证而不是密码认证。.ssh目录里的公钥变化也值得监控。你可以在管理机上写一个定时任务,每天检查所有authorized_keys文件的哈希值,出现变化马上告警,能第一时间发现“被莫名清理”或“被篡改”的情况。
5.4 关于密码短语和 ssh-agent 的搭配
最后聊一个特别容易造成“密钥过期”错觉的细节:私钥文件本身设置了密码短语,每次登录都要输入一次。很多人早上开机后第一次连服务器输入了密码短语,之后一直可以免密登录,某天突然又要求输入,第一反应是“密钥失效了”。其实只是 ssh-agent 在重启后还没有加载这把钥匙。
把这段加到~/.ssh/config里:
Host * AddKeysToAgent yes IdentityFile ~/.ssh/id_ed25519再配合系统启动时执行一次:
ssh-add ~/.ssh/id_ed25519这样重启后只要输入一次密码短语,后续整个会话周期内都免密。如果你有多个不同安全级别的密钥,可以分开Host段,避免所有钥匙一股脑进 agent。
我在实际维护中还有一个习惯:每把用于生产环境的密钥,都会在注释里写明用途、负责人、创建时间。比如用ssh-keygen -C "ops-bastion-2025-zhangsan"生成。这样即使几年后被人翻出来,也至少能知道这把钥匙是谁的、是干什么的,而不是面对一堆yourname@yourhost的默认注释发愁。很多“过期”问题,最后不是技术解决不了,是信息对不上。把元信息写清楚,能省掉一大半排查时间。