说实话,我看到好几个技术交流群里都在转“SSH密钥过期”这个话题,点进去一看,大部分人都把问题理解窄了。很多人一看到“过期”两个字,就以为是密钥本身有个保质期,到时间自动失效。这个说法对,但也不全对——要是你用的是最普通的SSH Key,它本身确实没有内置的失效时间;但你偏偏连不上服务器了、没法推送代码了,那这个“过期”的感觉又从哪来?
这就要说到真实的坑了。过去这段时间,我前前后后帮人排查了十几个类似的问题,有的是GitLab代码推不上去,有的是Gerrit审查系统登不进去,还有的是自己云服务器突然要密码了。原因五花八门,但表象都指向同一件事——密钥不“认”了。所以这篇东西我不想只讲一个点,而是想顺着“密钥过期”这个由头,把从生成、配置、验收到排查的整条链路都过一遍。你踩过的坑、还没踩的坑,我尽量都写到。
1. 先搞清楚:SSH密钥到底会不会过期
1.1 普通SSH密钥的“有效期”是个伪命题
先说结论:你自己执行ssh-keygen生成的那对密钥,私钥和公钥文件里是没有“有效期”这个字段的。只要私钥文件不丢、不被覆盖、不被删除,理论上十年后它还是同一把钥匙。
那为什么大家会碰到类似“密钥过期”的报错?最常见的情形是服务端拒绝了你,而客户端又没给出足够明确的提示。比如你推送代码到GitLab时,跳出这么一串:
code复制git@gitserver: Permission denied (publickey).
fatal: Could not read from remote repository.
看到这个,第一反应是密钥坏了、被吊销了、过期了。其实这里的Permission denied (publickey)只说明一个问题:服务端在你的公钥列表里,找不到能匹配你这次连接所用的私钥。也就是说,这不叫过期,叫“不被识别”。
为什么不被识别?要么是公钥压根没传到服务器上,要么是传错了账户,要么是你本机SSH登录时选错了私钥文件,要么是客户端软件升级后默认算法变了、不再加载旧的密钥类型。
1.2 新式SSH证书才真正“自带过期时间”
如果你在大型公司或严格管理的基础设施团队待过,可能接触过另一种东西——SSH Certificate,也就是由CA签发的SSH证书。这种证书里面确实包含valid-from和valid-to时间字段,到点就失效,没有任何商量余地。这是为了满足安全合规要求,强制员工定期轮换身份凭证。
现在不少云厂商、内部堡垒机系统都支持这种证书登录方式。当CA签发的证书过期时,你连上去就会看到:
code复制Certificate invalid: expired
或者服务端日志里出现类似“key is not valid”的记录。这种情况下的“过期”才是真正意义上的过期。
但日常开发者和中小团队用得最多的,还是GitLab、Gerrit、GitHub或者自建Git服务器上的普通SSH Key。所以这篇内容的重点,也放在普通密钥的“准过期”问题排查上。当你把所有可能性都排掉之后,如果确实用了证书体系,再单独确认CA那边的时间策略也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 自查秘钥状态:先确认问题出在本地还是远端
2.1 检查本地私钥是否还在、是否被软件加载
遇到连不上的情况,我的习惯是先在本机做一轮快速体检,不要一上来就重新生成密钥。重新生成密钥意味着你要把新公钥重新配到所有服务器上,成本很高。排查顺序很关键。
第一步,确认私钥文件还在:
bash复制ls -l ~/.ssh/
正常情况下,这里面会有id_ed25519和id_ed25519.pub这样的成对文件,也可能是id_rsa和id_rsa.pub。如果你看到私钥文件没了,但.pub文件还在,那说明私钥确实丢了。这种情况下别犹豫,只能重新生成一对新密钥,然后把新公钥灌到所有目标服务器上。
第二步,用ssh-add -l查看当前会话中SSH Agent加载了哪些私钥:
bash复制ssh-add -l
如果你之前用ssh-add ~/.ssh/id_ed25519把私钥加进过Agent,重启电脑或重启终端后Agent缓存会被清空。这时候你去连服务器,客户端可能找不到对应的私钥,看起来就像“密钥失效了”。但其实你把密钥重新加一遍就好:
bash复制eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
这个细节最容易让人误判。尤其是Mac用户,开机后第一次打开终端推送代码,报Permission denied,十有八九就是Agent没加载密钥。别急着去服务器改配置,先加一下密钥试试。
2.2 用Verbose模式看连接过程中的真实反馈
如果本地密钥文件都在,Agent也加载了,但还是连不上,那就用调试模式直接看SSH握手过程发生了什么。这是排查的关键一步,比你在网上搜半天报错都有效。
bash复制ssh -vT git@gitserver.com
-v是verbose的意思,它会打印出SSH连接全过程。如果一次不够,还可以用-vv甚至-vvv,越多的v代表越详细的调试信息。
重点看这几行输出:
code复制debug1: Offering public key: /Users/you/.ssh/id_ed25519 ED25519 SHA256:xxxx...
debug1: Authentications that can continue: publickey
debug1: No more authentication methods to try.
如果看到“Offering public key”后面跟着的是你的私钥路径,但服务端仍然拒绝,说明你提供的这把公钥没在目标服务器的授权列表里。如果是GitLab或Gerrit,那问题大概率出在网页端配置的SSH Key没粘对,或者粘到了别的用户账号下。
如果输出里根本没有“Offering public key”这一行,而是跳过了你的私钥,直接尝试其他认证方式,那就说明SSH客户端没把你这把私钥作为候选。常见原因是私钥权限不对,或者客户端配置里写了IdentitiesOnly、指定了错误的IdentityFile路径。
2.3 一个容易忽略的点:known_hosts变动带来的错觉
还有一种情况,服务器重装过系统或者换了IP,SSH客户端会发现服务器的主机指纹和~/.ssh/known_hosts里记录的不一致,于是弹出警告:
code复制@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
很多人看到这个提示会误以为自己的密钥过期了,因为之前明明能连,现在突然被拦住。其实这是SSH在防止中间人攻击,跟你本人的密钥没有关系。解决办法是找到known_hosts里对应的旧记录并删掉:
bash复制ssh-keygen -R 服务器IP或域名
然后再连一次,选择接受新的主机指纹即可。注意,如果你不确定服务器是否真的重装过,先别急着删记录,最好找管理员确认一下,避免连到了冒牌服务器。
3. 重新生成SSH密钥并完成GitLab/Gerrit配置
3.1 为什么现在推荐ED25519而不是RSA
如果排查下来确实是私钥丢了、或者旧密钥在服务端被清理掉了,那就需要重新生成密钥。现在的推荐项跟十年前不太一样了,别再习惯性地敲ssh-keygen -t rsa了。
目前最稳妥的选择是:
bash复制ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519
ED25519使用EdDSA算法,密钥更短、生成更快、安全性也更高,而且现代主流系统、GitLab、Gerrit、GitHub都支持得非常好。如果你是2020年以后才开始用SSH,那完全没有理由再回到2048位或4096位的RSA老路上。
但有一个场景要留意:如果你的服务器端SSH版本比较老,比如CentOS 6自带的OpenSSH 5.3,可能不支持ED25519。这时候只能用RSA。怎么确认?在服务器上执行:
bash复制ssh -V
如果版本号低于6.5,建议用RSA。不过都2025年了,这种老古董环境少之又少,能用ED25519就用ED25519。
生成过程中会提示你设置passphrase,也就是私钥口令。这里我直接给建议:别为了方便留空。虽然每次用密钥都要输一遍口令挺烦的,但你可以把私钥加到SSH Agent里,只需要输一次。留空的私钥一旦泄露,等于把服务器大门钥匙直接交给别人。尤其是云服务器,公网上一堆扫描器在抓弱密钥,别赌这个概率。
3.2 公钥该往哪里填:GitLab与Gerrit配置实录
生成完密钥后,打开公钥文件:
bash复制cat ~/.ssh/id_ed25519.pub
输出的内容是一整行,格式大致是:
code复制ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIXXXX... your_email@example.com
复制的时候要完整复制整行,不要漏掉开头的ssh-ed25519和后边的注释。然后打开你的GitLab页面,按下面路径操作:
- 点击右上角头像,进入“Preferences”或“设置”
- 左侧菜单找到“SSH Keys”或“密钥”选项
- 把公钥粘贴到“Key”输入框
- “Title”随便填一个方便识别的名称,比如“Work MacBook Pro”
- 如果服务器设置了密钥有效期(有些团队会启用这个限制),按需选择过期时间;没要求就不选
- 点击“Add key”保存
Gerrit的配置思路类似,但入口略有不同。登录Gerrit后,点击右上角你的用户名,进入“Settings”,左侧找到“SSH Keys”,把公钥粘进去保存。Gerrit页面上通常还会显示一个HTTP密码,用于HTTPS方式推代码,那个跟SSH无关,别搞混了。
配置完后测试连通性。GitLab的测试命令是:
bash复制ssh -T git@gitlab.com
如果看到类似:
code复制Welcome to GitLab, @yourusername!
说明已经通了。Gerrit的话,不同实例差异比较大,有的返回空内容但退出码是0,有的会打印欢迎信息。判断标准是echo $?执行后的退出码,0就代表认证成功。
注意:无论GitLab还是Gerrit,在页面上添加公钥时,一定确认是登录了正确的账号。尤其是公司内部有多套Git服务时,经常有人把A系统的公钥粘到B系统的账号下,结果反复配置就是连不上。
3.3 服务器上的authorized_keys到底该怎么写
如果你管理的是自己的Linux服务器,不是GitLab或Gerrit这种平台,那就得手动把公钥加到服务器的~/.ssh/authorized_keys文件里。这个文件里的格式同样是一行一条公钥。把公钥追加进去的命令:
bash复制cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
这行命令做了四件事:创建.ssh目录、设置目录权限为700、把公钥写进authorized_keys、把文件权限设为600。权限这个点极其重要。很多人配置完连不上,一查原因,发现authorized_keys的权限是644甚至777,SSH服务端出于安全考虑会直接忽略这个文件。
有些服务器还开着SELinux,光改权限还不够。如果是CentOS/RHEL系列,用以下命令检查SELinux状态:
bash复制getenforce
如果返回的是Enforcing,而且文件是从别的地方拷贝过来的,可能需要重置一下文件上下文:
bash复制restorecon -Rv ~/.ssh
这个坑很隐蔽,尤其在你习惯了Ubuntu服务器之后,突然去搞一台CentOS,很容易在这里卡半天。
4. 客户端SSH配置:避免密钥选择错乱的细节
4.1 多密钥场景下IdentitiesOnly的妙用
当你的~/.ssh目录下有多把密钥时,SSH客户端默认会按顺序尝试所有私钥,直到服务端认可其中一把。这个机制偶尔会“好心办坏事”:比如你同时有公司GitLab和个人GitHub的密钥,连接公司服务器时客户端先用个人私钥去试探,被拒绝了才换公司的私钥,虽然最终能连上,但多了一轮不必要的尝试,而且在某些严格配置的服务器上,多次尝试会被直接拉黑。
更稳妥的做法是为不同的服务器指定专属密钥。在~/.ssh/config文件里写清楚:
code复制Host gitlab.company.com
HostName gitlab.company.com
User git
IdentityFile ~/.ssh/id_ed25519_company
IdentitiesOnly yes
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_home
IdentitiesOnly yes
这里的IdentitiesOnly yes作用很关键。它的含义是告诉SSH客户端:只使用IdentityFile指定的这把密钥,不要拿Agent里的其他密钥去试探。没有这个参数时,即使你指定了IdentityFile,SSH还是有可能把Agent里加载的所有密钥都提交一遍。
4.2 检查~/.ssh目录权限,别让客户端拒绝加载
SSH客户端对私钥文件的权限要求非常严格。私钥文件权限如果太开放,比如644或777,SSH会直接拒绝使用这把私钥。所以每次复制密钥文件到新机器后,都要记得重置权限:
bash复制chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
目录权限700的意思是只有自己能进入,文件权限600的意思是只有自己能读写。公钥文件之所以可以用644,因为它本来就是要给服务器看的,不存在保密需求。
Windows用户如果用OpenSSH,同样遵循这套权限规则。但Windows的权限管理比较复杂,文件从资源管理器里右键设置属性不一定管用,建议直接在PowerShell里用icacls命令处理,省得出幺蛾子。
4.3 大坑预警:SSH Agent缓存导致的旧密钥残留
再讲一个非常容易踩的坑。假设你重新生成了一对新密钥,也把新公钥配置到了GitLab上,但本机的SSH Agent里还缓存着旧私钥。连接时客户端先把旧私钥抛给服务器,服务器一脸懵;因为客户端没有加IdentitiesOnly yes,于是继续尝试新私钥,最终连上了。看起来一切正常,但服务器日志里会留下多次认证失败的记录。
如果是Gerrit这类安全审计比较严格的系统,多次失败可能导致IP被临时封禁。你还在纳闷为什么突然连不上,其实是把自己的IP封了。
解决办法是清理Agent缓存,把旧密钥踢出去:
bash复制ssh-add -D
ssh-add ~/.ssh/id_ed25519_new
-D表示清空所有已加载的密钥,然后再把新私钥加进去。这个操作在换新密钥后必须做,否则你会在各种诡异的问题里浪费大量时间。
5. 常见“假过期”问题排查实录与避坑指南
5.1 时间不同步,证书认证被误判过期
有个特别容易被忽略的因素:客户端机器时间不准。如果你的电脑时间跟真实时间偏差太大,碰到使用SSH Certificate的服务端时,时间校验环节就直接挂了。哪怕你的证书明明还在有效期内,客户端或服务端也会把它当成已过期。
检查本机时间同步状态,macOS可以执行:
bash复制sntp -s time.apple.com
Linux的话,用timedatectl查看,用ntpdate或chronyc手动同步。这类问题好解决,但排查起来确实浪费时间。建议先把系统时间校准了,再去试SSH连接,省得怀疑人生。
5.2 修改过用户名导致密钥与账号失联
还有一种场景我不是第一次见了:同事离职后,他原来用的服务器账号被管理员删掉,新同事接手时自己生成密钥,但管理员把公钥放错了账号。又或者本机改了Git的user.name和user.email之后,推代码一切正常,但实际上远端用的是SSH协议,跟你Git配置的姓名邮箱没有半点关系。很多人被这个误导,以为改了Git配置就能换权限,结果一直卡着。
重点说清楚:SSH连接的身份由你的密钥决定,不是由git config里的用户名决定。服务器认的是公钥,配了哪个账号的公钥,就以哪个账号的身份访问。哪怕你的Git提交信息写的是“张三”,只要SSH用的是李四的公钥,服务端就认为你是李四。
这个搞明白后,很多“为什么我推不上去”的问题就迎刃而解了。先看自己用的是哪把私钥,再看这把私钥对应的公钥在远端账号里有没有配置。两头都对上,才能通。
5.3 故障排查速查手册
整理了这么多次问题,我总结了一张速查表,碰到SSH认证失败直接对着查:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| Permission denied (publickey) | 公钥未配置到远端账号 | 重新粘贴公钥并确认账号正确 |
| 服务器提示“Offering public key”后仍失败 | 公钥匹配不上 | 检查是否把公钥配到了别的账号 |
| 连接直接超时 | 网络或防火墙问题 | 换端口测试、联系管理员 |
| 提示REMOTE HOST IDENTIFICATION HAS CHANGED | 服务器主机指纹变化 | ssh-keygen -R 目标IP清除旧记录 |
| 客户端加载密钥时报权限错误 | 私钥文件权限太开放 | chmod 600 ~/.ssh/id_ed25519 |
| 证书认证提示expired | 证书过期或系统时间不对 | 检查CA签发的有效期,校准本机时间 |
| Agent加载了旧的私钥 | 新密钥未生效 | ssh-add -D后重新添加新私钥 |
5.4 实在查不出问题时,稳妥的终极办法
如果以上所有方法都试过了,还是连不上,那就走一遍“重新来”流程。这个流程虽然繁琐,但能保证100%排除本地配置问题:
- 备份现有
~/.ssh目录到临时文件夹 - 从零生成一对新密钥:
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519 - 把新公钥内容复制到GitLab/Gerrit/服务器
- 清空Agent缓存并加载新私钥
- 用
ssh -vT测试连接并观察输出
这一套流程相当于把本地到远端整条链路重置了一遍。大多数“假过期”问题在这一步都会被解决掉。如果这样还不行,那就把-v输出的日志贴给管理员看,别自己瞎折腾了。运维那边能看到服务端日志,往往一眼就能看出问题在哪。
6. SSH密钥日常维护:几个值得养成的好习惯
6.1 给密钥加上注释并分场景管理
生成密钥时的-C参数很简单,作用却很大。它相当于给公钥加了一个备注。当服务器上有几十个公钥时,一个清晰的注释能让你快速定位是哪台机器、哪个用途的密钥。
比如你有一台工作电脑和一台家用电脑,分别生成密钥:
bash复制ssh-keygen -t ed25519 -C "work-macbook-pro" -f ~/.ssh/id_ed25519_work
ssh-keygen -t ed25519 -C "home-desktop" -f ~/.ssh/id_ed25519_home
在GitLab或Gerrit里配置时,Title直接写上对应名称。日后清理不用的密钥时,一眼就能认出该删哪把。别小看这个细节,等账号越加越多,你会感谢当初分得清楚的自己。
6.2 定期审视远端账号里的密钥列表
很多人的GitLab或Gerrit账号里堆了一堆旧公钥,有些是多年前的电脑,有些是已经离职同事帮忙配置的临时密钥。密钥越多,安全风险越大。因为只要其中一把私钥泄露,攻击者就能拿到你账号的SSH访问权限。
建议每隔半年做一次密钥审查:
- 登录GitLab/Gerrit/服务器的密钥管理页面
- 逐把检查公钥是否还在使用
- 删除所有不认识的、不用的、来路不明的公钥
- 确认剩余的每把密钥都能对应到具体的人和设备
对于GitLab,还可以开启“过期密钥自动禁用”策略,在管理后台设置密钥最大有效期。这样即使团队成员忘了轮换,也会在过期前收到提醒。当然,这需要管理员权限,普通开发者只能自己管好自己的那几把密钥。
6.3 私钥泄露后的正确应急流程
万一私钥泄露了,别慌张,也别只删远端公钥就完事。私钥泄露后的正确处理顺序是:
- 立即在远端账号删除对应的公钥
- 在本地生成一对新密钥
- 把新公钥配置到所有需要访问的服务器/平台
- 如果泄露的私钥同时用于其他服务器,所有服务器都要替换
- 检查远端账号的登录日志,确认有无异常操作
- 如果怀疑有人用泄露密钥做过操作,及时通知安全管理员
千万不能只做第1步就收工。公钥删了,但泄露的私钥还在攻击者手里,如果它同时能解开其他服务器的认证,危害照样存在。所以重新生成新密钥并全面替换,才是真正的“止血”。
