先说个上周刚踩的坑。晚上十点,正准备把改好的代码推到服务器,结果ssh连过去直接给我甩了一句Permission denied (publickey)。我当时脑子里第一个念头就是:坏了,密钥是不是过期了?
这个念头其实挺常见的。很多人配好SSH密钥之后,就把它当成一个“一劳永逸”的东西,哪天突然连不上了,第一个怀疑对象就是密钥过期。但根据我这些年搞运维和服务器管理的经验,SSH密钥真正“过期”的概率非常低,大多数时候是被各种各样相似又隐蔽的问题给坑了。这篇文章就按“速查”的思路来写,说清楚SSH密钥过期的真相、排查路径,以及怎么把密钥轮换做成一个不慌的流程。
1. 先说结论:SSH密钥本身不会过期,但确实存在真实的“有效期”机制
1.1 OpenSSH密钥为什么没有“有效期”字段
很多人以为SSH密钥像软件许可证一样,到时间就自动失效。实际上,用一个简单的命令就能看出来:
ssh-keygen -l -f ~/.ssh/id_ed25519.pub输出通常是一行指纹信息,包括密钥类型、长度、指纹、注释,但没有“过期时间”这个概念。因为OpenSSH默认生成的RSA、Ed25519这类密钥文件,在协议层面就没有内置有效期字段。它就是一个用数学算法生成的不对称密钥对,私钥留在本地,公钥放到服务器,只要私钥文件还在、口令还记得,理论上可以一直用下去。
那为什么网上总有人说“密钥过期”?有一种情况是管理员在服务器的authorized_keys文件里给某个密钥加了限制,例如expiry-time="2024-12-31"这样的选项。较新版OpenSSH支持给单个公钥设置有效期、来源IP、强制执行的命令等限制,这时候被限制的密钥到了时间确实会提示鉴权失败。但这种情况属于管理员主动“没收”了你的密钥,不是密钥自己“老了”。
1.2 真正会“到期”的变种:证书认证与平台托管密钥
另外两种会真正过期的情况,在实际工作中也挺常见。
第一种是SSH证书认证。很多公司不直接在服务器上放公钥,而是用一台CA证书签发服务器,给每个员工签一张短期有效的用户证书,比如:
ssh-keygen -s ca_key -I user01 -V +52w user01_key.pub这里的-V +52w意思是证书有效期52周。证书到期后,即使你本地私钥没变,服务器也会拒绝接入。这种情况下,查看证书有效期可以用:
ssh-keygen -L -f ~/user01_key-cert.pub第二种是托管平台的密钥策略。GitHub、GitLab这些平台对SSH Key会有“最后使用时间”的标记,长期不用的密钥可能在后台收到提醒,甚至被运维策略强制清理。加上很多公司做安全加固,会把超过一年没动的key从账号里删掉。你本地还留着私钥,但平台侧公钥已经不在了,表现就是突然连不上。
1.3 十次里有八次不是过期,而是这些“假过期”场景
我自己处理过的“SSH密钥过期”工单里,真正到期的情况非常少,大概率是下面这几种:
- 服务器重装过了,或者运维重新初始化了
authorized_keys,你的公钥被覆盖掉了; - 你换了电脑、换了用户名,ssh默认加载的私钥路径变了,系统根本没读到你的key;
- 私钥文件还在,但权限不对(比如变成了644甚至666),OpenSSH出于安全考虑直接忽略掉;
- 服务器上
/home目录或.ssh目录属主错误,比如root拥有了home目录,sshd直接拒绝用该用户的公钥鉴权; - 服务器IP被回收,连接到了另一台机器,
known_hosts检出主机密钥不匹配,报REMOTE HOST IDENTIFICATION HAS CHANGED; - 本地ssh-agent里加过私钥,后来没有重新加载,报
Agent admitted failure to sign; - 密码登录被禁用后,公钥也没配置好,典型表现就是连密码框都不弹,直接拒绝。
所以看到“密钥过期”这个词,我更愿意把它理解为“密钥不再被信任”,而不是“文件过期”。接下来就把排查路径写清楚。
2. 本地快速自查:5分钟定位是不是密钥文件的问题
2.1 先看密钥文件还在不在、权限对不对
遇到连不上,先看本地~/.ssh目录:
ls -la ~/.ssh正常情况下,私钥文件的权限应该是600,公钥文件644,整个.ssh目录700。OpenSSH对权限非常敏感,尤其是私钥,如果权限太宽松,它会认为这个文件“不安全”,直接在调试信息里说不加载这个key,表现就是服务器端看起来像没有这个公钥一样。如果发现权限不对,马上改:
chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub另外一个常见坑是多密钥管理。如果电脑上有id_rsa、id_ed25519、github_key等多个密钥文件,ssh默认只按顺序尝试默认名字的key,不会自动把所有的key都试一遍。如果需要的那个key名字不标准,就要在~/.ssh/config里写清楚:
Host myserver HostName 192.168.1.100 User ubuntu IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes这里IdentitiesOnly yes很关键,它告诉ssh只使用指定的密钥,不要尝试agent里的其他key,能避免很多“明明配好了却老是被拒绝”的怪问题。
2.2 加调试参数,让SSH告诉你卡在哪一步
猜来猜去不如直接看日志。连接时加上-vvv参数:
ssh -vvv user@server输出会非常详细,重点看三行:
debug1: Offering public key: /home/you/.ssh/id_ed25519 debug1: Server accepts key: /home/you/.ssh/id_ed25519 debug1: Authentications that can continue: publickey如果第一行之后直接跟Authentications that can continue,说明服务器没有接受这把key,问题基本在服务器端authorized_keys里没有对应公钥,或者权限不对。如果压根没看到Offering public key,说明本地没有找到合适的私钥,优先检查密钥路径、ssh-agent和config配置。
还有一点值得提:很多人习惯直接看服务器日志,但其实本地调试输出已经能定位七八成问题。先本地看,再上服务器查,效率最高。
2.3 ssh-agent与会话级“过期”
如果你用了ssh-agent,还得单独检查一下:
ssh-add -l它列出当前agent里加载了哪些key。如果没有key,或者报Agent admitted failure to sign using the key,说明agent里没有可用的私钥,或者私钥口令不对。重新加一次就行:
ssh-add ~/.ssh/id_ed25519这里有个容易忽略的点:有些人会给ssh-agent配置超时时间,比如ssh-agent -t 3600,意思是加载的key一个小时后自动失效。这种属于“会话过期”,重新ssh-add就能恢复,但它确实会让人产生“密钥过期了”的错觉。服务器上长期跑着的自动化任务如果用了这种agent,还得注意定时刷新。
2.4 VSCode远程连接连不上怎么办
VSCode Remote-SSH连不上,很多人第一反应也是“SSH密钥过期了”。其实Remote-SSH在服务器端会装一个~/.vscode-server目录,里面缓存了插件和运行环境,一旦这个目录权限不对或文件损坏,表现就是连上后立刻断、或者反复要求输入密码。
处理办法很简单:在VSCode里执行命令Remote-SSH: Kill VS Code Server on Host,把服务器端缓存清掉,再重新连接。通常这一步就能解决。另外,VSCode的Remote-SSH本质还是用OpenSSH,如果本地ssh直连都有问题,先按上面2.1和2.2排查,别在插件设置里钻牛角尖。
3. 服务器端排查:密钥“看起来过期”的真正原因
3.1 公钥到底有没有在 authorized_keys 里
本地确认没问题后,就要上服务器看authorized_keys文件。默认路径是用户家目录下的~/.ssh/authorized_keys,但要注意sshd_config里可能修改过AuthorizedKeysFile配置。可以先看下实际配置:
grep -i authorizedkeys /etc/ssh/sshd_config确认路径之后,检查自己的公钥是否在文件里:
cat ~/.ssh/authorized_keys如果文件空了,或者里面没有你的公钥,那问题就是你被“踢出”了信任列表,重新把自己的公钥追加进去即可:
echo "ssh-ed25519 AAAA... your@email" >> ~/.ssh/authorized_keys这里特别提醒:authorized_keys文件必须由当前登录用户拥有,权限应该是600,.ssh目录权限700。群里很多同学喜欢顺手chmod -R 777,这会让sshd直接拒绝读取这个文件,报错信息是bad ownership or modes for directory。
3.2 用户目录与密钥文件权限
权限问题非常隐蔽,也是“假过期”的头号嫌疑人。OpenSSH有严格的安全校验逻辑,家目录、.ssh目录、authorized_keys文件,任何一级被其他用户可写,它就认为存在安全隐患,拒绝使用密钥登录。常见的报错有:
Authentication refused: bad ownership or modes for directory /home/user出现这个,按顺序检查:
ls -ld /home/user ls -ld /home/user/.ssh ls -l /home/user/.ssh/authorized_keys正确的状态是:家目录一般是755,不能是777;.ssh目录是700,属主是当前用户;authorized_keys是600,属主是当前用户。万一发现是root等其他人所有,要改回来:
chown -R user:user /home/user/.ssh chmod 700 /home/user/.ssh chmod 600 /home/user/.ssh/authorized_keys这种问题在从别的机器拷贝、打包解压、或者用root批量推送公钥的时候特别容易触发,我至少踩过三次。有一次我为了图省事,直接root登录后用cp把一个备份的公钥目录覆盖回去,结果家目录属主全变成了root,用户根本没权限读自己的密钥目录,表现就是“密钥突然失效了”。
还有NAS的场景,群晖这类系统上配置SSH密钥时更要注意路径和权限,因为NAS的home目录结构、默认Shell、甚至是否加载~/.ssh的机制都和普通Linux发行版不完全一样。配置之前先用echo $HOME确认当前用户家目录路径,再把公钥放到正确的位置,否则文件放对了地方却读不到,特别容易误判成过期。
3.3 sshd日志:最诚实的“裁判”
本地和服务器的文件都检查过了还找不到原因,就去看sshd日志。Debian/Ubuntu系列通常写在/var/log/auth.log,RHEL/CentOS系列通常写在/var/log/secure,如果用了systemd,也可以直接:
journalctl -u ssh -f或者:
journalctl -u sshd -f日志会明确告诉你拒绝的原因。有一次我排查了很久,最后发现日志里写的是User ubuntu not allowed because account is locked,这是因为服务器上有人把用户的密码清空了,导致账户处于锁定状态,虽然公钥是正常的,sshd还是拒绝了连接。还有一次日志里全是Connection closed by authenticating user,最后发现是fail2ban把出口IP临时封禁了,和密钥半毛钱关系都没有。
所以排查看日志,尽量别猜。日志里出现的常见关键词主要有:
Failed password for invalid user:公网扫描很频繁,说明有人在暴力尝试,不代表你有问题;Connection reset by IP:可能被防火墙或fail2ban拦截;Authentication refused: bad ownership or modes:权限不对;User xxx not allowed because account is locked:账户锁定;Accepted publickey for user from IP port xxx ssh2: ED25519 SHA256:xxx:这是正常成功的标志,看到这行说明密钥被接受。
另外,如果服务器上日志里频繁出现大量Failed password,说明这台机器正被公网扫描盯上,虽然通常是常规操作,但建议配合fail2ban或防火墙做一下访问控制,避免哪天把正常登录的IP也误伤封掉,看起来就像密钥失效了一样。
3.4 用户过期与账户锁定
这里再补充一个容易忽略的维度:系统用户本身有过期时间。用chage查看:
chage -l username输出里的Password expires、Account expires如果显示为过期的日期,也会导致登录异常。虽然这属于账户层面的问题,但使用者感知到的就是“我连不上了,是不是密钥不行”。排查密钥问题的时候,顺手看一眼账户状态,能省不少事。
4. 密钥生命周期管理:怎么让“过期”这事可控
4.1 密钥轮换的完整实操
与其每次等出了故障再手忙脚乱,不如把密钥轮换做成固定流程。我一般推荐的步骤是:
- 生成新密钥对,推荐用Ed25519算法,性能好、密钥短、安全性高:
ssh-keygen -t ed25519 -a 100 -C "your-name-$(date +%Y%m%d)" -f ~/.ssh/id_ed25519_2025-C参数是注释,建议带上你的名字和生成日期,以后回头看指纹能知道这把key是谁、什么时候建的。
- 把新公钥推送到服务器:
ssh-copy-id -i ~/.ssh/id_ed25519_2025.pub user@server如果没有ssh-copy-id,可以手动追加,上面3.1里已经写过命令。
用新密钥测试登录,确认能正常进去之后,再考虑清理旧公钥。
在服务器上编辑
authorized_keys,删掉旧的公钥行,完成切换。
这里有个实操习惯:别一次性把所有服务器的旧key删掉。先把新key推到所有需要登录的机器,确认没问题了,再逐个清理旧的。遇到服务器特别多的情况,建议用Ansible这类工具统一推公钥,authorized_key模块天然具备幂等性,不会重复追加,也比自己写脚本循环更安全。
4.2 Git平台上的密钥“过期”
GitHub、GitLab、Gitea这些平台上的SSH Key同样有生命周期问题。平台后台都会记录每个Key的最后使用时间,长时间不用的Key容易被安全策略清理。我自己遇过一次GitLab账号里某个旧Key被管理员定期清理任务删掉,本地git pull直接报权限错误,当时还一度以为是公司网络问题。
平时可以定期在平台后台看一眼Key列表,把不用的删掉,给留用的Key补充说明文字。GitHub也支持给Key加Title,建议写成“设备-用途-日期”的格式,比如home-macbook-work-20250101,这样每次安全审计的时候,一眼就能看出哪些Key该清理了。另外,平台Web后台通常还支持查看Key的指纹,可以和本地ssh-keygen -l -f ~/.ssh/xxx.pub的输出比对,确认平台上的Key和本地私钥严格对应。
git的密钥配置流程也经常有人踩坑。这里说一个常规做法,先自己执行ssh-keygen生成密钥,再在平台上添加公钥,本地验证用:
ssh -T git@github.com如果Git平台端口不是22,或者公司网络屏蔽了22端口,可以用ssh -T -p 443 git@ssh.github.com这类带端口的连接方式测试,并检查~/.ssh/config里是否写对了Host和HostName。很多时候不是密钥问题,而是连接配置和网络问题,别一上来就删除重建。
4.3 批量管理大量服务器时的密钥更新
如果手上有几十台上百台服务器,别一台台手动推公钥了。我平时用Ansible比较多,写一个简单的playbook就能完成:
--- - hosts: all tasks: - name: 部署运维公钥 authorized_key: user: ops state: present key: "{{ lookup('file', '/home/ops/.ssh/id_ed25519.pub') }}"这种方式的好处是,公钥集中在一个地方维护,服务器出了问题随时可以重新推。没有Ansible、只想用Shell快速跑的话,也可以写个循环调用ssh-copy-id,但要注意把服务器列表和密钥路径管理好,别弄混。
批量场景里还有一个容易踩的坑:很多云服务器的救援模式、初始化脚本里会固定写死一个初始公钥,后续你用普通方式改了authorized_keys,但如果系统重新初始化了,老公钥又会回来。这时候你会发现明明自己“删掉”了某个Key,过几天它又出现了,这不是密钥问题,是云平台的初始化逻辑在作祟。遇到这种情况,别在服务器上较劲,去改云平台的初始化配置或镜像模板。
4.4 把密钥文件变成可审计、可恢复的资产
密钥真正的风险不是“过期”,而是“失管”。自己电脑上的私钥丢了还能重新生成,最怕的是服务器上存了一堆不知道谁在用、什么时候加的废旧公钥。我建议每半年做一次密钥盘点:
- 在服务器上列出
authorized_keys里所有公钥的指纹:
while read key; do echo "$key" | ssh-keygen -lf -; done < ~/.ssh/authorized_keys- 和团队维护的密钥登记表比对,找不到主人的公钥直接清掉。
- 重要服务器的
authorized_keys文件加个监控,文件变化就在团队群里告警。
这样一来,密钥的问题从“某天突然连不上”变成“定期检查在掌握之中”,就算哪天真的发现自己的key被清理,因为有备份清单,也能很快重新加回去。
5. 常见问题排查速查表与避坑清单
5.1 高频场景速查表
下表整理了典型的SSH密钥连接异常现象、可能原因、首选排查命令和解决思路,建议遇到问题直接翻:
| 现象 | 可能原因 | 首选排查命令 | 解决思路 |
|---|---|---|---|
Permission denied (publickey) | authorized_keys里没有对应公钥 | ssh -vvv user@server | 重新推送公钥ssh-copy-id |
bad ownership or modes for directory | 家目录、.ssh或authorized_keys权限不对 | ls -ld ~ ~/.ssh | chmod 700 ~/.ssh; chmod 600 authorized_keys |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED | 服务器主机密钥更换或重装系统 | ssh-keygen -R server_ip | 通过带外方式确认指纹后重新连接 |
Agent admitted failure to sign using the key | ssh-agent未加载私钥或密钥口令错误 | ssh-add -l | 重新执行ssh-add 私钥路径 |
Load key "..." invalid format | 私钥文件内容被截断或从Windows粘贴时格式变化 | file ~/.ssh/xxx; ssh-keygen -y -f ~/.ssh/xxx | 重新生成或从备份恢复 |
Connection closed by IP port 22 | IP被fail2ban或防火墙封禁 | journalctl -u ssh | 检查封禁列表,申请解封或更换出口IP |
| git pull/fetch权限失败 | Git平台上的Key被删除或未添加 | ssh -T git@github.com | 在平台后台重新添加公钥 |
| 密码登录被关且公钥未配置 | sshd_config中PasswordAuthentication no | 查看服务器端日志 | 用救援通道登录并配置公钥 |
关于known_hosts的报错,单独多说一句。WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED是“主机密钥”变更,不是你的登录密钥过期了。服务器重装系统、容器重建、或者IP被分配给另一台机器都会触发。处理方式也不是直接忽略,而是先通过云控制台、带外管理口等方式确认这台服务器确实是你要连的那台,然后才执行ssh-keygen -R清理旧记录,重新连接获取新的主机指纹。如果盲目跳过认证,等于放弃了防中间人攻击的最后一道防线,这个习惯真的很危险。
5.2 我踩过的坑和教训
排查SSH密钥问题,最大的敌人是“想当然”。我第一次遇到密钥过期问题时,花了两天时间反复在本地重新生成密钥,结果最后发现是服务器上有人把authorized_keys文件改名为authorized_keys.bak,真正的文件被覆盖了。后来我学乖了,遇到连接失败先看本地调试输出,再上服务器看日志,两步基本能定位问题。
另外一个深刻教训是:Windows环境的密钥格式问题。从Windows记事本里复制私钥粘贴到别处,很容易在行尾带上特殊字符,导致私钥解析失败,报Load key invalid format。正确做法是用scp把密钥文件整个拷贝过去,不要用复制粘贴大法。
还有就是,改动服务器配置前一定先备份。修改authorized_keys、sshd_config这些关键文件前,先把原文件复制一份:
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak.$(date +%F)这样即使操作出错,也能快速回滚,不至于把自己锁在门外。
5.3 防患于未然的几个习惯
最后分享几个我日常坚持的习惯,能大幅降低“密钥过期”这类问题的发生率:
- 新电脑配好密钥后,先在本机用
ssh -T连一次Git平台,确认链路没问题再继续干活; - 给私钥设置口令(passphrase),但要用ssh-agent记住,避免每次连接都输密码;
- 把公钥放到一个集中管理的配置仓库里,服务器初始化时统一部署,不搞临时手动追加;
- 服务器上只用公钥登录,关闭密码登录,能有效减少被暴力破解的风险,也让密钥问题暴露得更早;
- 定期检查
authorized_keys和平台Key列表,超过一年不用的Key直接清理。
我经常跟同事说,密钥这玩意就像家里钥匙,不是它哪天突然“坏了”,而是你可能换过锁、搬过家、扔过备用钥匙,最终打不开门的原因五花八门。排查的顺序永远是从自己近端开始,再往远端查,最后再看中间网络和安全策略。
如果你现在正好被“SSH密钥过期”卡住,别急着重新生成密钥。先看一眼本地调试日志,再去服务器翻一下日志,多半能找到真正的原因。就算最后确认是密钥文件损坏,也有备份和轮换流程兜底,不至于被困在门外。