项目标题是“从零到一:配置SSH密钥打通Git与GitLab的认证通道”。这属于纯技术工具配置范畴,内容是合法的工程实践。下面按博主口吻写这篇博文,覆盖密钥原理、算法选型、实操步骤、多账号管理、排错速查和密钥生命周期。
新机器到手,第一件事往往不是装 IDE,而是把代码仓库连上。我见过太多人卡在这一步:git clone敲下去,密码框弹一次、两次、三次,最后被令牌过期拦住,一上午的时间全耗在认证上。说到底,SSH 密钥这套东西就是把“每次证明你是谁”变成“一次配置、长期可信”,而Git与GitLab之间的通道,本质上只有一件事——服务端手里握着你的公钥,客户端手里握着私钥,两边对上一次暗号就放行。这篇内容写给三类人:刚接触命令行、还在用账号密码拉代码的新手;手里管着好几个仓库、天天被多账号认证搞晕的开发者;以及需要给团队定一套密钥规范的技术负责人。我会把原理、参数选择、命令细节、报错排查全部摊开讲,包括那些文档里不会写、只有真踩过才知道的坑。
1. 为什么我坚持用 SSH 而不是 HTTPS 拉代码
1.1 一次配置、长期免密的真实收益
先说结论:HTTPS不是不能用,而是它的认证成本会随着时间线拉长而不断累积。用 HTTPS 克隆仓库,第一次要输账号密码,后来平台改成要求个人访问令牌,令牌有有效期,到期那天你的git pull就会突然失败,而失败信息往往只有一句Authentication failed,你还得去翻是哪个月配的令牌、权限勾了哪些、过期时间写在哪。更麻烦的是,令牌一旦泄露,攻击者拿到的权限和你的账号完全等价,而你很难第一时间察觉。
SSH 密钥的模型不一样。它用非对称加密做身份证明,私钥永远只存在你自己的机器上,连服务端都拿不到。服务端存的只是公钥,公钥泄露没有任何风险,因为它只能用来加密发给你的挑战,不能反推出私钥。这就把“凭据泄露”这个最大的风险面直接砍掉了一半。
收益具体体现在几个场景里。第一是日常提交:配置好之后,git push不再有任何交互,脚本里、别名里、编辑器插件里调用 git 都不会卡在认证上。第二是自动化:CI 流水线、定时备份脚本、部署机拉代码,这些场景根本没有人坐在那里输密码,密钥是唯一可行的方式。第三是多仓库并行:一个人同时维护公司内网实例、外部托管平台、开源社区三个来源的代码,HTTPS 下你要来回切凭据,SSH 下用一份配置文件就能全部路由清楚。
我自己的习惯是:凡是需要长期、高频访问的仓库,一律走 SSH;只有临时看一眼别人的公开仓库、或者在不方便放私钥的临时环境里,才用 HTTPS 加只读令牌。这个分界线定下来之后,认证相关的故障率下降得非常明显。
1.2 认证链路的底层逻辑:挑战应答而不是传密码
很多人配完密钥能用,但从来没想过中间发生了什么,所以一旦报错就完全无从下手。这里用生活化的方式讲清楚。
想象服务端有一把只能锁不能开的挂锁——这就是公钥。你把锁寄给服务端,服务端把它挂在门上。你要进门时,服务端不会问你要钥匙,而是随机找一个盒子,用那把锁锁上,递给你。你手里有唯一的钥匙(私钥),能打开盒子,取出里面的随机数,再签个名还回去。服务端一看签名对得上,就知道你确实持有钥匙。整个过程钥匙从未离开你的口袋,盒子里的随机数每次都不一样,截获了也没法重放。
映射到协议层面,大致是这样几步:客户端先发起连接,声明“我想用这个公钥认证”;服务端在自己的authorized_keys列表里查找有没有匹配的指纹,有就生成一段随机数据用公钥加密后发回;客户端用私钥解密,再把解密结果和会话标识一起做签名,回传;服务端用公钥验签,通过则建立会话。中间任何一环对不上,最终都会收敛成那句让人抓狂的Permission denied (publickey)。
理解了这个流程,排查就有了方向:是公钥没送到位,还是私钥没被客户端选中,还是本地文件权限太宽导致客户端拒绝使用私钥,还是网络层根本没连到 22 端口。这四个问题对应四套完全不同的解法,后面第 5 章我会逐条拆。
提示:认证失败时先别急着重新生成密钥。九成以上的问题出在“公钥没挂上”“私钥没被选中”“权限过宽”这三件事上,重新生成往往只是把问题复制了一遍。
1.3 算法选型:Ed25519、RSA 4096 与 ECDSA 该怎么挑
ssh-keygen默认生成什么,取决于你机器上的 OpenSSH 版本。老版本默认 RSA 2048,新版本默认 Ed25519。这个默认值背后有实打实的差异,值得花两分钟搞清楚。
| 算法 | 典型密钥长度 | 安全强度大致相当于 RSA | 生成速度 | 签名验签性能 | 兼容性 | 建议 |
|---|---|---|---|---|---|---|
| Ed25519 | 固定 256 位 | RSA 3072 左右 | 极快,毫秒级 | 很快 | OpenSSH 6.5 以上、主流托管平台均支持 | 首选 |
| RSA | 2048 / 4096 位 | 2048 位强度偏弱 | 4096 位可能耗时数秒到数十秒 | 较慢 | 几乎所有老系统都支持 | 老环境兜底 |
| ECDSA | 256 / 384 / 521 位 | 256 位约等于 RSA 3072 | 快 | 快 | 支持面略窄于 RSA | 不作为首选 |
| DSA | 1024 位 | 已不安全 | 快 | 快 | 新版 OpenSSH 默认禁用 | 不要使用 |
具体到参数计算,RSA 的安全强度取决于大整数分解难度。2048 位目前仍在“未被实际攻破”的范围内,但安全边际已经不大;3072 位是常用的过渡档;4096 位安全边际充足,代价是生成慢、握手时的计算开销高,在低配机器或高并发场景下能明显感觉到延迟。Ed25519 走的是椭圆曲线,密钥短、运算快、没有可调参数带来的“弱参数”风险,如果你没有特殊兼容性要求,直接用它。
我在实际项目里的做法分两档:个人日常开发、云主机之间互信,全部用Ed25519;对接一些年代久远的内网系统、或者对方平台明确只认 RSA 时,才生成RSA 4096。至于 2048 位的 RSA,除非是某个无法升级的遗留系统强制要求,我不会主动生成。
还有一个细节:Ed25519的密钥文件内容比 RSA 短很多,私钥大概几百字节,便于在受限环境里导入导出,但这也意味着它不能像 RSA 那样通过加长密钥来提升强度——它的强度是固定的,所以更要注意私钥本身的保管。
2. 生成密钥之前的准备工作
2.1 目录结构、文件权限与命名规范
~/.ssh这个目录是 OpenSSH 的地盘,它对这个目录和里面文件的权限有硬性要求。私钥文件如果被同组用户或其他用户可读,客户端会直接拒绝使用,并给出bad permissions或UNPROTECTED PRIVATE KEY FILE的警告。这不是吹毛求疵,而是防止同一台多用户机器上别人顺手把你的私钥复制走。
标准权限是这样的:~/.ssh目录700,也就是只有属主能读、写、进入;私钥600;公钥644问题不大,但很多人习惯也设成600,更保险;config、known_hosts、authorized_keys一般600或644都可以,authorized_keys严格一点更好。
命名规范这件事,单账号的人觉得无所谓,多账号的人会感谢自己。默认生成的是id_ed25519,个人机器上只有一个身份时完全够用。但只要你有第二个身份,比如公司内网实例一个、外部托管平台一个,同名文件就会互相覆盖或者被混淆。我的命名习惯是id_ed25519_<用途>,例如id_ed25519_work、id_ed25519_personal,公钥就是同名加.pub。这样在config里引用时一目了然,半年后回来也不会认错。
顺手提一个容易被忽略的点:~/.ssh目录的属主必须是你自己。有些人用sudo生成过密钥,结果文件属主变成 root,之后普通用户身份怎么都用不了,报错信息还特别隐晦。生成前先ls -la ~/.ssh看一眼,属主不对就先chown回来。
2.2 passphrase 到底设不设
这是被问得最多的问题之一。设了,每次开机或每个会话要输一次;不设,私钥文件被拿走就等于身份被盗。我的答案是:看这台机器的暴露面。
如果是你自己独占的笔记本,磁盘加密开着、开机有登录密码、平时不装来路不明的软件,那么不设 passphrase 是实际可行的,配合ssh-agent也能做到既免密又相对安全。如果是一台共享的开发机、跳板机、或者放在公共办公区的台式机,必须设。因为在这些环境里,“文件被复制走”是一个现实概率不低的场景。
设了 passphrase 之后的麻烦,ssh-agent能解决大半。你把私钥解密后加载进代理,代理在内存里持有解密后的密钥,后续连接由代理代签,你只需要在开机后第一次使用时输一次口令。macOS 上可以配合钥匙串做到开机自动加载,Linux 桌面环境一般也能集成,Windows 上则是通过 OpenSSH Authentication Agent 服务实现。
还有一个中间方案:给不同用途的密钥设不同强度的口令,高权限的部署密钥设长口令并且只在特定机器上使用,日常开发密钥的口令可以短一些。这种分级思路比“一刀切”更贴近真实工作需要。
注意:passphrase 不等于账号密码,它只保护私钥文件的解密过程,输错不会锁定账号,也没有次数限制的服务端校验。但一旦忘记,私钥就再也用不了,只能重新生成并重新上传公钥。设之前想清楚,设完找个可靠的地方记下来。
2.3 ssh-keygen 参数逐个拆解
命令很短,但每个参数都有意义,值得一条条过。
ssh-keygen -t ed25519 -C "你的标识信息" -f ~/.ssh/id_ed25519_work-t指定算法类型,这里用ed25519。如果想用 RSA,就换成-t rsa并配合-b 4096。-b是位数参数,只对 RSA、ECDSA 这类可变长度算法有效,Ed25519是固定长度,加了-b反而会报错,这一点新手特别容易踩。
-C是注释,会原样写进公钥末尾。它的实际作用是在服务端的密钥列表里帮你识别“这是哪台机器的哪把钥匙”。很多人随手写个邮箱,结果半年后账户里挂了七八个公钥,自己都分不清哪个该删。我的建议是写成“用途@设备@日期”的形式,比如work@laptop-2024,排错和清理时价值极高。
-f指定输出文件路径。不指定的话会交互式询问,还默认写到id_ed25519,容易和多身份场景冲突。直接指定路径,省去后续改名和配置调整的麻烦。
命令执行后会提示输入 passphrase,直接回车表示不设。这里有个细节:提示会问两次,第一次输入、第二次确认,两次不一致会要求重来。整个过程屏幕上不会显示任何字符,包括星号,这是正常设计,不是卡住了。
生成完成后目录里会出现两个文件:id_ed25519_work是私钥,绝对不要外传、不要贴到聊天窗口、不要提交进仓库;id_ed25519_work.pub是公钥,内容是一行文本,可以随便复制粘贴到任何地方。这个区分必须刻进肌肉记忆,我见过把私钥内容整段贴进工单系统的,那等于把家门钥匙拍照发群里。
3. 从生成到验证的完整实操流程
3.1 生成密钥并核对指纹
接着上一步,密钥生成后第一件事是核对指纹。指纹是公钥内容的哈希摘要,用来确认“我手里这把”和“服务端挂的那把”是不是同一把。
ssh-keygen -lf ~/.ssh/id_ed25519_work.pub输出大概是256 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxx work@laptop-2024 (ED25519)这样的格式。前半段是位数,中间是 SHA256 指纹,后面是注释和算法。把这个指纹和服务端密钥列表里显示的指纹比对,一致就说明配对成功。这一步在多设备、多次上传的场景里特别重要,能避免“传错文件”这种低级但高发的错误。
顺便说一下查看公钥内容的方式:
cat ~/.ssh/id_ed25519_work.pub输出是单行,以ssh-ed25519开头,中间是一长串 Base64,末尾是注释。复制时一定要完整复制整行,包括开头的算法标识。常见错误是只复制了中间那段 Base64,或者复制时把换行符带进去了,服务端校验就会失败,提示“不是有效的 SSH 密钥”。
文件权限也顺手确认一下:
chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519_work chmod 644 ~/.ssh/id_ed25519_work.pub这三条命令建议形成条件反射,尤其是从别的机器拷贝密钥过来之后,权限信息往往不会跟着过来,必须重新设。
3.2 把公钥挂到服务端的账户上
服务端这边,GitLab的入口在用户设置里的 SSH 密钥页面。粘贴公钥内容,填一个能让你日后认出来的标题,设置过期时间,保存。
过期时间这个字段很多人直接留空。我的建议是:个人长期使用的开发密钥可以不设或者设一年,到期前会提醒你轮换;给临时设备、给外包同事、给一次性环境配的密钥,必须设过期时间。这是成本最低的权限收敛手段。
粘贴时有两个高频问题。第一是多复制了空格或换行,导致校验失败;解决办法是先粘到一个纯文本编辑器里,删掉首尾空白,再整行复制。第二是误把私钥粘进去,有些平台会直接拒绝并提示格式不对,但也有一些不会那么明确,所以务必确认文件名带.pub。
保存后页面通常会显示这把密钥的指纹,拿它和 3.1 节算出来的指纹比对,一致就说明这条链路的前半段通了。
还有一类场景是项目级的部署密钥。它不属于任何个人账户,只对单个项目有权限,通常设为只读,用在构建机拉代码的场景。这种密钥的好处是权限边界清晰,坏处是管理分散,项目一多就需要一套台账。我在团队里推的做法是:所有部署密钥统一登记在内部文档里,记录用途、归属人、创建日期、过期时间,每季度清一次。
3.3 用 config 文件把连接参数固定下来
~/.ssh/config是整个 SSH 体系里最被低估的文件。大部分教程只教你敲ssh -T git@host,但真实工作里主机名长、端口不同、密钥多把,每次都在命令行拼参数不现实。
一个典型配置长这样:
Host gitlab-work HostName gitlab.internal.example.com User git Port 22 IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes ServerAliveInterval 30 ServerAliveCountMax 3逐行说。Host是你自己起的别名,之后所有命令都可以用这个别名代替真实地址,比如ssh -T gitlab-work。HostName是真实主机名,注意这里不要写git@,用户名由User那行负责。User git是绝大多数 Git 服务平台约定的固定值,不是你的账号名,写错了会一直认证失败。Port默认 22,如果服务端用了别的端口就改这里。
IdentityFile指定这把别名用哪把私钥。IdentitiesOnly yes是个关键参数,它告诉客户端“只用我指定的这把,别把我代理里其他密钥一把把试过去”。不设这个参数时,如果代理里加载了五六把密钥,客户端会挨个尝试,服务端可能因为尝试次数过多直接断开,报Too many authentication failures。这个坑我在多账号环境里踩过不止一次。
后面两行ServerAliveInterval和ServerAliveCountMax跟网络稳定性有关。前者是每隔多少秒发一个保活包,后者是连续多少次没响应就断开。在不稳定的网络下,默认配置可能在你思考的间隙就把连接掐了,加上这两行能明显改善体验。数值不用太激进,30 秒一次比较合适。
配置文件写完后记得改权限chmod 600 ~/.ssh/config,然后可以用ssh -G gitlab-work打印出最终生效的配置,检查有没有写错或者被其他Host段覆盖。
3.4 代理托管与开机自动加载
ssh-agent的作用是把解密后的私钥留在内存里,避免每次连接都重新输 passphrase。基本操作是三步。
启动代理并让当前 shell 认识它:
eval "$(ssh-agent -s)"加载私钥:
ssh-add ~/.ssh/id_ed25519_work查看已加载的密钥:
ssh-add -lssh-add -l的输出会列出每把密钥的位数、指纹和文件路径,指纹应该和 3.1 节算出来的一致。如果提示Could not open a connection to your authentication agent,说明当前 shell 没有继承代理的环境变量,重新执行一次eval那行即可。
不同系统的自动化方式差别挺大。macOS 上可以在~/.ssh/config里加AddKeysToAgent yes和UseKeychain yes,再配合ssh-add --apple-use-keychain,把口令存进系统钥匙串,重启后自动加载。Linux 桌面环境一般由桌面会话自动拉起代理,把ssh-add写进 shell 的启动脚本就能生效,但要注意别在非交互式 shell 里重复执行导致报错。Windows 上需要启用系统服务里的 OpenSSH Authentication Agent,并设为自动启动,然后把AddKeysToAgent配上。
一个容易忽略的细节:代理的生命周期和登录会话绑定。你在终端 A 里eval启动的代理,终端 B 是不认识的,除非两者共享环境变量。所以更稳妥的做法是让系统级的代理统一管理,而不是手动在某个窗口里启动。
3.5 验证连通性并切换仓库地址
配置全部就位后,用一条测试命令验证:
ssh -T git@gitlab-work注意用户名是git,这是服务端的约定。成功的话会返回一句欢迎信息,带上你的用户名,说明认证通过、但因为没有分配 shell 所以不提供交互式登录,这是正常的。如果返回Welcome to GitLab, @username!之类的内容,链路就彻底通了。
失败时的第一反应应该是加-v:
ssh -vT git@gitlab-work这会打印完整的握手过程,包括协商的算法、尝试了哪些密钥、服务端返回了什么。看输出里的Offering public key和Server accepts key两行,前者列出客户端尝试的密钥,后者表示服务端接受了哪一把。如果Offering里根本没有你期望的那把,说明IdentityFile或代理配置有问题;如果Offering有但紧接着是Permission denied,说明服务端没挂对应的公钥,或者挂的不是同一把。
最后是把已有仓库的地址从 HTTPS 切到 SSH。先看当前地址:
git remote -v然后改:
git remote set-url origin git@gitlab-work:group/project.git这里的冒号后面是项目路径,和 HTTPS 地址里的路径部分一致,注意不要写成git@host/group/project.git,斜杠和冒号的区别是新手最容易错的地方。改完再跑一次git remote -v确认,然后git fetch测试。
如果仓库很多,可以用一条全局配置批量替换:
git config --global url."git@gitlab-work:".insteadOf "https://gitlab.internal.example.com/"这样以后所有用旧地址克隆或拉取的操作都会被自动改写成 SSH 地址,省去逐个仓库改的麻烦。
4. 多账号多平台并存的密钥管理
4.1 Host 别名隔离法
一个人同时有三四个代码来源太正常了:公司内网实例、外部托管平台、某个开源社区、再加一台自建的测试环境。如果都用默认的id_ed25519,必然互相覆盖;如果都用真实主机名,config里的Host段容易写重复,导致匹配顺序出问题。
别名隔离是标准解法。给每个来源起一个短别名,比如gitlab-work、gitlab-personal、git-community,每个别名对应一套HostName + User + IdentityFile。之后所有 git 地址都用别名写:git@gitlab-work:team/project.git。这样地址本身就带上了身份信息,人和机器都不会搞混。
这里有个匹配顺序的坑值得单独说。config文件是从上往下匹配的,第一个匹配上的Host段生效。如果先写了一个通配的Host *段,后面的具体别名就可能被它抢走。正确做法是把具体别名放前面,通配段放最后,只用来设置全局的保活参数和AddKeysToAgent这类通用项。
另一个细节是同一个主机名、不同账号的情况,比如你在某个平台上既有个人账号又有团队账号。这时HostName相同,靠的是Host别名和IdentityFile区分,User那行依然是git。服务端通过公钥反查是哪个账户,所以关键是两把密钥都挂到了各自账户上,不能挂错。
4.2 IdentitiesOnly 与尝试次数超限
Too many authentication failures这个报错,本质上是客户端太“殷勤”。代理里加载了多把密钥时,客户端会挨个尝试,而服务端通常在若干次失败后直接断连。表现出来就是明明有一把正确的密钥,却始终连不上。
根因是 OpenSSH 默认会遍历所有可用密钥,而不是只用配置里指定的那一把。IdentitiesOnly yes就是关掉这个遍历行为,强制只用IdentityFile指定的密钥。这个参数我建议写在每个具体别名的段里,而不是只写在通配段,避免被覆盖。
还有一种情况是仓库级别指定密钥。有些团队不希望改个人全局配置,就在仓库的本地配置里指定:
git config core.sshCommand "ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes -F /dev/null"-F /dev/null的作用是忽略全局config文件,完全靠命令行参数决定行为,适合在脚本或特定仓库里做隔离。但要注意,这样写之后该仓库会失去全局配置里的保活参数,长连接场景可能要补上-o ServerAliveInterval=30。
临时性的场景可以用环境变量:
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_ed25519_work" git clone git@gitlab-work:team/project.git这个方式不改任何配置文件,适合一次性操作。
4.3 项目级部署密钥与构建环境
个人密钥和项目密钥要分清楚。个人密钥代表“你这个人”,能访问你有权限的所有项目;部署密钥代表“这个项目”,通常只读,且只对单个项目有效。在构建机、发布机、容器镜像里,绝对不该塞个人私钥,应该用部署密钥。
配置部署密钥时,服务端一般会有“是否允许写权限”的开关。默认关掉,需要推送才打开。我的经验是能只读就只读,构建阶段拉代码不需要写权限,多给一分权限就多一分风险。如果确实需要推送,比如自动打标签、自动提交版本号,那就单独建一把只对目标项目有写权限的密钥,并且设置过期时间。
容器里使用密钥有个额外的坑:镜像层会缓存文件。如果把私钥COPY进镜像,即使后面RUN rm删掉,历史层里依然能挖出来。正确做法是在构建时通过挂载或构建参数临时注入,或者用多阶段构建,确保私钥不落在最终镜像的层里。
另外,构建环境的known_hosts是空的,第一次连接会提示确认主机指纹,非交互环境下会直接失败。解决办法是提前把服务端指纹写进去:
ssh-keyscan -t ed25519 gitlab-work >> ~/.ssh/known_hosts但要注意,ssh-keyscan本身不验证服务端身份,理论上存在中间人风险。更稳妥的做法是首次在可信环境里连接一次,把known_hosts里的那行记录下来,作为固定内容分发到构建环境。
5. 常见报错排查实录
5.1 Permission denied 的六种成因
这个报错是 SSH 领域的“万能错误”,信息量几乎为零,必须靠外围排查。我把它拆成六种情况,按出现频率排序。
第一种,公钥压根没上传。有人生成了密钥但只复制了私钥路径,忘了把.pub的内容贴到服务端。检查方式是对比本地指纹和服务端显示的指纹,不一致就是没传对。
第二种,传了但传错了账户。多账号环境里,把工作密钥传到了个人账户,或者反过来。这种情况指纹能对上但账户不对,表现是连接成功但拉不到代码。
第三种,私钥没被客户端选中。config里没配IdentityFile,或者配了但代理里有别的密钥抢先尝试。用ssh -vT看Offering public key那几行,确认尝试的是不是你期望的密钥。
第四种,文件权限过宽。客户端检测到私钥可被其他用户读取,直接跳过使用。报错里通常会有bad permissions字样,chmod 600解决。
第五种,服务端禁用了该类算法。极少数老平台只支持 RSA,你传了 Ed25519 就不认。换一把 RSA 4096 的密钥再试。
第六种,网络层没通。22 端口被防火墙拦了,表现是连接超时而不是认证失败,但粗心的人容易混为一谈。用nc -vz host 22或telnet host 22测一下端口连通性,能连上才谈得上认证问题。
提示:排查顺序建议从外到内——先测端口,再看
ssh -vT的密钥列表,再比对指纹,最后查权限和服务端配置。倒着查会浪费大量时间。
5.2 主机指纹变更与 known_hosts 冲突
换过服务器、重建过实例、或者平台做过迁移之后,再次连接会遇到一段很长的警告,大意是“远程主机标识已变更,可能存在中间人攻击”。这是known_hosts里存着旧指纹导致的。严格来说这个警告不该无脑跳过,因为它确实可能是攻击信号。
但在实际工作里,绝大多数情况就是服务端换了机器。稳妥的处理方式是:先向平台管理员确认是否做过主机变更,确认之后再删除旧记录:
ssh-keygen -R gitlab-work这个命令会从known_hosts里移除对应条目,下次连接重新确认。注意这里要用你config里的别名,而不是真实主机名,因为写入known_hosts的是连接时使用的名字。
如果连接时用的端口不是 22,known_hosts里的记录会带上方括号和端口号,删除时也要带上,否则删不掉。
5.3 排查速查表
| 现象 | 最可能原因 | 快速验证 | 处理方式 |
|---|---|---|---|
Permission denied (publickey) | 公钥未上传或账户不匹配 | 对比本地与服务端指纹 | 重新上传正确公钥 |
bad permissions警告 | 私钥权限过宽 | ls -l ~/.ssh/ | chmod 600私钥 |
Too many authentication failures | 代理中密钥过多 | ssh-add -l查看数量 | 配置IdentitiesOnly yes |
Connection timed out | 端口不通或被拦截 | nc -vz host 22 | 确认网络策略与端口 |
Host key verification failed | 服务端指纹变更 | 查看警告中的新旧指纹 | 确认后ssh-keygen -R |
Could not open a connection to your authentication agent | 代理未启动或环境变量丢失 | echo $SSH_AUTH_SOCK | 重新eval启动代理 |
| 认证成功但拉取提示无权限 | 密钥对但账户权限不足 | 服务端查看项目成员权限 | 申请对应项目权限 |
| 每次都要输 passphrase | 密钥未加载进代理 | ssh-add -l无输出 | ssh-add加载并配置自动加载 |
这张表我贴在过团队的内网文档里,新人自助解决的比例明显上升,省下了不少重复答疑的时间。
6. 密钥生命周期与日常安全习惯
6.1 轮换、吊销与审计
密钥不是配完就一劳永逸的东西。我的习惯是给每把密钥定一个心理有效期:个人开发密钥一年一换,部署密钥半年一换,临时设备用完即删。换的时候不是简单生成新的,而是先在服务端加上新公钥、验证通过、再删掉旧公钥,中间保持可用,避免把自己锁在门外。
审计这块,GitLab的密钥列表里会显示每把密钥的标题、指纹、创建时间、最后使用时间。定期扫一眼,重点看三类:不认识的标题、长期未使用的、没有过期时间的。长期未使用往往意味着对应设备已经不用了,留着就是纯风险。
还有一个容易忽略的点:密钥是跟着设备走的。换电脑、重装系统、把旧机器转手之前,一定要记得去服务端把对应公钥删掉。我见过不少人旧笔记本卖了、密钥还挂在账户上,等于把钥匙留在了别人手里。
吊销这个概念在 SSH 里没有证书体系那么正式,本质上就是删除服务端的那条记录。删除之后,持有私钥的人也进不来,这是唯一有效的“吊销”手段。所以发现私钥可能泄露时,动作要快:立刻删服务端记录,然后检查该密钥关联的所有平台和项目,逐个清理。
6.2 几个我真实踩过的坑
第一个坑是sudo生成密钥。早年在一台服务器上直接sudo ssh-keygen,生成的文件属主是 root,之后用普通用户身份怎么都认证失败,报错信息里没有任何关于权限属主的提示。后来ls -la才发现问题。教训是:永远用实际使用该密钥的那个用户身份生成,不要图省事加sudo。
第二个坑是复制公钥时多带了一个换行。服务端保存后看起来正常,指纹也和本地一致,但认证就是不通过。原因是一些平台对内容做了严格校验,尾部空白导致解析出的密钥格式有问题。后来养成习惯,复制前先粘到纯文本编辑器里,确认是单行、无首尾空白、以算法名开头。
第三个坑是config里的Host段顺序。有次我在文件开头写了个Host *用来统一设置保活参数,结果后面所有具体别名都被它先匹配走了,IdentityFile完全没生效,所有连接都在用默认密钥。排查了半天才意识到是顺序问题。正确做法是具体别名在前、通配段在最后。
第四个坑是容器构建时把私钥带进了镜像层。当时用COPY把整个.ssh目录拷进去,构建完RUN rm -rf删掉,以为没事,结果扫描工具在历史层里找到了私钥。后来改成构建参数注入加多阶段构建,才算解决。
第五个坑是 passphrase 忘了。设的时候觉得简单,三个月后重装系统要重新加载,怎么都想不起来。最后只能重新生成、重新上传、重新改配置。现在我的做法是把口令记在密码管理器里,并且给密钥注释加上创建年月,方便判断哪些该轮换了。
这些坑说起来都不复杂,但每一个都实打实花掉过半天时间。密钥配置这件事,难点从来不在命令本身,而在环境差异、历史遗留和细节疏忽上。把ssh -vT的输出读懂,把config的匹配顺序搞明白,把权限和指纹当成必查项,基本就能覆盖九成以上的场景。至于剩下那一成,多半是服务端那边的配置或网络策略问题,这时候拿-v的日志去问管理员,比一句“我连不上”有效得多。