SSH 密钥配置这件事,说小很小,一条ssh-keygen加一个粘贴框,五分钟能搞定;说大也真大,我见过有人卡了整整一个下午,就因为把私钥内容粘进了 GitLab 的公钥输入框,然后对着Permission denied (publickey)反复怀疑人生。这篇 GitLab SSH 密钥配置指南,就是把我这些年从生成、粘贴、验证到排错的完整链路摊开讲一遍,包括每一步为什么这么做、参数为什么这么选、报错信息应该从哪一行开始读。它适合三类人:刚接手 Git 仓库、还在用账号密码反复输入的新手;手上有两三个 GitLab 账号、remote老是串号的多人协作场景;以及需要把密钥塞进流水线、给部署流程做免交互认证的工程实践者。读完你至少能独立完成一次干净的密钥配置,并且下次再遇到认证失败,知道该敲哪条命令去定位,而不是盲目删文件重来。
1. 先想清楚:为什么 GitLab 上要用 SSH 而不是账号密码
1.1 一次推送背后的认证账本
很多人对 SSH 的理解停留在"免密"两个字,其实免密只是结果,真正的差别在认证模型。走 HTTPS 协议推送时,你每次操作都要向服务端证明"我是这个账号的持有者",凭据是密码或者访问令牌,这个凭据是可以被复制、被记录、被中间环节截获的字符串。而 SSH 走的是密钥对认证:本地保存一个私钥,服务端只存你的公钥,推送时服务端发一段随机挑战,客户端用私钥签名,服务端用公钥验签。私钥从头到尾不出本机,网络上传输的只有签名结果,签名还带时效,被截获也没有重放价值。
这个差别在个人电脑上感知不强,一进团队环境就很明显。访问令牌通常有较长的有效期,一旦写进某个脚本、某个配置文件、某次误提交,就等于把仓库的读写权限拱手让人,事后只能靠翻审计日志倒查。密钥对虽然也有泄露风险,但私钥文件默认躺在~/.ssh下,权限收紧到 600,配合口令加密,泄露门槛高出一截,而且撤销动作极其干脆——服务端删掉公钥,对应的私钥立刻变成一张废纸。
还有一层实际收益容易被忽略:交互体验。用令牌走 HTTPS,第一次输完可以缓存,但缓存有超时,换个终端、重启一次机器、进一次容器,又要重新输。SSH 借助 agent 管理会话内的私钥,一天下来推送几十次都不用再碰键盘。对我这种一天要切四五次仓库的人来说,这个差异直接决定了工作节奏顺不顺。
1.2 密钥对到底在认证什么
把流程拆开看:你在本地生成一对数学上关联的密钥,公钥可以随便公开,私钥必须保密。你把公钥上传到 GitLab,服务端把它和你的账号绑定,存进数据库。之后每次推送,SSH 客户端向服务端发起连接,声明"我想用这把公钥登录",服务端在自己库里找到这把公钥,就用它加密一段随机数据发回去;客户端用私钥解密,再把结果和一个会话标识一起做哈希,回传给服务端校验。校验通过,服务端确认"持有对应私钥的人,就是当初上传公钥的人"。
关键点在于:服务端从头到尾没有见过你的私钥,也无法从公钥反推出私钥。这也是为什么公钥泄露不构成安全问题——它本来就是要公开的。真正需要保护的是私钥文件本身,以及私钥的口令。理解了这一层,后面所有关于权限、口令、agent 的操作就有了统一解释:它们都在围绕"让私钥更难被拿走"这一个目标展开。
还有一个容易踩的认知坑:GitLab 的 SSH 地址里用户名固定是git,不是你自己的账号名。git@gitlab.example.com:group/repo.git这一串里,git只是一个约定俗成的登录标识,真正决定你是谁的是那把被服务端匹配上的公钥。所以当你看到git@却报权限错误时,别去怀疑用户名写错了,问题一定出在密钥没匹配上。
1.3 哪些场景必须切换到 SSH
少数场景下 HTTPS 更省事,比如只需要匿名克隆公开仓库,或者所在环境对出站端口有严格限制只放行 443。但只要涉及写操作,SSH 的优势就会压过来。
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 日常开发推送、拉取 | SSH | 一次配置长期有效,无令牌过期烦恼 |
| 自动化脚本、定时任务 | SSH(专用密钥) | 免交互,凭据可独立撤销 |
| 流水线拉取私有依赖 | SSH 部署密钥 | 权限可收窄到单项目、只读 |
| 临时查看公开仓库 | HTTPS | 零配置,不落任何凭据 |
| 受限网络仅放行 443 | HTTPS | SSH 默认 22 端口可能被拦截 |
补充一句端口的事:SSH 默认走 22,部分企业网络会限制非常用端口的外连。GitLab 通常还提供一个备用 SSH 端口(常见是 2222),遇到连不上先别急着怀疑密钥,用ssh -T -p 2222 git@你的域名试一下,能通就说明是端口策略问题,改~/.ssh/config里的Port即可,和密钥配置没有半点关系。
2. 生成密钥对:算法、长度和口令的取舍
2.1 ed25519 和 RSA 该怎么选
打开任何一篇教程都会看到-t rsa -b 4096,这是历史惯性。RSA 用了很多年,兼容性最好,但密钥体积大、签名慢、握手时交换的数据多。ed25519 是基于椭圆曲线的现代算法,公钥只有 68 个字符,签名速度快,同等安全强度下体积小得多,现在的 GitLab、主流代码托管平台和 OpenSSH 7.0 以上版本全都支持。
我的默认选择是 ed25519,除非遇到明确的兼容性障碍。什么算障碍?老旧的嵌入式设备、某些只认 RSA 的老版本 SSH 实现、或者所在环境有强制合规要求只允许特定算法。这几种情况用ssh-keygen -t rsa -b 4096兜底,4096 位是当前实践中的合理下限,2048 位虽然还能用,但留给未来的余量太少,新生成没必要省这一步。
# 首选:现代椭圆曲线算法 ssh-keygen -t ed25519 -C "dev@example.com" -f ~/.ssh/id_ed25519_gitlab # 兼容兜底:RSA 4096 ssh-keygen -t rsa -b 4096 -C "dev@example.com" -f ~/.ssh/id_rsa_gitlab-C后面的注释纯粹是给人看的标记,会附加在公钥末尾,方便你在 GitLab 的密钥列表里一眼认出这把钥匙属于哪台机器。我习惯写成用途-设备-年份的形式,比如work-macbook-2024,半年后回头看列表,不至于对着一堆id_ed25519发呆。
-f指定输出路径也是强烈建议加上的。默认路径~/.ssh/id_ed25519是全局唯一的,一旦你同时有个人账号和团队账号,两把密钥抢同一个文件名,只能靠备份覆盖来回折腾。按用途命名,从一开始就避开这个坑。
2.2 生成命令逐项拆解与实操现场
完整跑一遍,看看终端都问了什么。
$ ssh-keygen -t ed25519 -C "work-macbook-2024" -f ~/.ssh/id_ed25519_gitlab Generating public/private ed25519 key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /Users/you/.ssh/id_ed25519_gitlab Your public key has been saved in /Users/you/.ssh/id_ed25519_gitlab.pub The key fingerprint is: SHA256:9xQ2...vJ8 work-macbook-2024 The key's randomart image is: +--[ED25519 256]--+第一个问题是口令。这里我给的建议和很多教程相反:别留空。留空意味着私钥文件一旦被复制走(备份同步、误上传、共享机器),对方不需要任何额外信息就能冒充你推送代码。加了口令,私钥在磁盘上是加密状态,别人拿到文件也解不开。
有人担心加了口令每次都要输,其实这是没配 agent 的问题,不是口令的代价。下一节会讲怎么让 agent 在会话内记住它。我自己的做法是:个人机器上口令设得稍短但足够随机,团队共享的构建机器上用专用密钥并配合受控的 agent 转发,全程不落明文。
第二个是随机艺术图(randomart),纯视觉辅助,方便你用肉眼比对两台机器上的指纹是否一致,可以忽略。真正要记住的是那行SHA256:开头的指纹,后面排错对账时用得上。
生成完成后目录里会多两个文件:id_ed25519_gitlab是私钥,id_ed25519_gitlab.pub是公钥。用ls -l看一眼权限,私钥应该是 600,如果不对就手动改:
chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519_gitlab chmod 644 ~/.ssh/id_ed25519_gitlab.pub2.3 私钥权限:那个让新手一脸懵的 600
OpenSSH 有个硬性检查:如果私钥文件对其他用户可读,它会拒绝使用,报WARNING: UNPROTECTED PRIVATE KEY FILE!然后直接中断连接。这不是故意刁难,而是防御性设计——如果同机器上的其他账号能读到你的私钥,那密钥认证的安全前提就不成立了。
Linux 和 macOS 上chmod 600就完事。Windows 稍微麻烦,NTFS 的权限模型和 POSIX 不一样,chmod在 Git Bash 里不一定生效。稳妥的做法是进文件属性 → 安全 → 高级,把继承关系断掉,只保留你自己的账户,或者用命令行处理:
icacls "C:\Users\you\.ssh\id_ed25519_gitlab" /inheritance:r /grant:r "$env:USERNAME:(R)"还有一种情况值得提:把私钥放在同步盘里(网盘、笔记软件的附件目录)。同步客户端往往会改文件权限,或者在多台设备间复制出多份副本,等于把私钥的作用域无限放大。私钥就该老老实实待在本机的~/.ssh下,需要多设备就各自生成各自的公钥,别图省事复制文件。
注意:任何时候都不要把私钥内容粘贴到网页表单、聊天窗口或者在线转换工具里。判断标准很简单——文件名以
.pub结尾的才是公钥,才可以外发。
3. 把公钥交给 GitLab:粘贴、校验与多账号管理
3.1 公钥长什么样,怎么正确复制
公钥文件是一行纯文本,结构是"算法标识 + base64 数据 + 可选注释",中间用空格分隔。看到ssh-ed25519 AAAAC3Nza...这样的开头就对了。文件里不允许有换行折行,整段必须在一行内——这是最常见的粘贴失败原因,尤其是在窄终端里cat出来,视觉上折行看着像多行,实际复制时可能带进了真实的换行符。
# macOS cat ~/.ssh/id_ed25519_gitlab.pub | pbcopy # Linux(需要 xclip) xclip -sel clip < ~/.ssh/id_ed25519_gitlab.pub # Windows Git Bash cat ~/.ssh/id_ed25519_gitlab.pub | clip用管道复制比手动框选安全得多,框选容易漏掉首尾字符,或者在鼠标拖拽时多选一个换行。如果只能手动复制,粘进编辑器后检查一遍:开头必须是ssh-ed25519或ssh-rsa,结尾是注释(可以是邮箱或者你写的标记),中间不能有空格断层。
还有一种低级但高频的错误:把私钥粘上去了。判断方法一眼就能看出来——私钥文件开头是-----BEGIN OPENSSH PRIVATE KEY-----,而且有几十行。GitLab 的表单看到这个会直接拒绝,报"无法生成指纹"之类的提示。养成习惯,粘贴前先head -c 30看一眼开头。
3.2 界面上的添加路径与到期时间
登录后点右上角头像,进入偏好设置里的 SSH 密钥页面,把公钥粘进大文本框,"标题"随便写但要能认出来,"到期日期"是可选但强烈建议填的。填个一年后,到期前系统会提醒,相当于给自己设了个密钥轮换的强制闹钟。团队里如果有人离职、设备丢失,到期机制也能兜住最后一层。
提交后页面会显示这把密钥的指纹和添加时间。指纹是公钥的 SHA256 摘要,你在本地可以用同样的方式算出来对账:
ssh-keygen -lf ~/.ssh/id_ed25519_gitlab.pub # 输出:256 SHA256:9xQ2...vJ8 work-macbook-2024 (ED25519)两边指纹一致,说明上传的就是本地这把,没有被编辑器改坏,也没有在半路被替换。
3.3 第一次连接验证与指纹确认
配完立刻验证,别等到推送的时候才发现问题。
$ ssh -T git@gitlab.example.com The authenticity of host 'gitlab.example.com' can't be established. ED25519 key fingerprint is SHA256:AbCd...XyZ. Are you sure you want to continue connecting (yes/no/[fingerprint])?这个提示是首次连接的正常现象,不是报错。它让你确认服务端身份,防止连到冒充的服务器上。正确做法是输入完整的yes(不是y),然后这段指纹会被记录进~/.ssh/known_hosts,以后不再询问。
输完yes之后如果看到Welcome to GitLab, @你的用户名!,说明整条链路通了,密钥匹配成功。注意这行问候语是专门给 SSH 连接用的,GitLab 不会给你 shell,-T参数就是告诉客户端不要申请交互终端,避免出现PTY allocation request failed的干扰信息。
如果不想被指纹提示打断脚本流程,可以在确认过指纹的前提下预先把主机指纹写入:
ssh-keyscan -t ed25519 gitlab.example.com >> ~/.ssh/known_hosts但这招在自动化环境里要谨慎用。ssh-keyscan拿到什么就写什么,中间如果有人做手脚,你信任的就是错误的指纹。更稳的做法是把已知指纹硬编码进脚本,或者用StrictHostKeyChecking=accept-new首次自动接受、后续严格校验。
3.4 多账号多密钥:config 文件是解药
一个开发者同时有个人账号、团队账号、客户项目账号,这在行业里非常普遍。默认情况下 SSH 客户端会按顺序把 agent 里所有密钥都试一遍,服务端通常只允许有限次失败尝试,试到第三把可能就被踢掉,报Too many authentication failures。解决办法是给每个主机建别名,明确指定用哪把钥匙。
# ~/.ssh/config Host gitlab-work HostName gitlab.example.com User git IdentityFile ~/.ssh/id_ed25519_gitlab_work IdentitiesOnly yes Host gitlab-personal HostName gitlab.example.com User git IdentityFile ~/.ssh/id_ed25519_gitlab_personal IdentitiesOnly yes Port 2222IdentitiesOnly yes是关键那一行,它强制客户端只用IdentityFile指定的密钥,不再挨个尝试 agent 里的其他钥匙。少了这行,前面说的认证次数超限问题照样会发生。
配好之后,仓库地址也要跟着改。原来克隆下来的是git@gitlab.example.com:group/repo.git,现在改成别名形式:
git remote set-url origin gitlab-work:group/repo.git注意别名后面直接跟路径,冒号前是别名而不是域名。这个细节很容易写错——gitlab-work会去读 config 里的配置,git@gitlab-work:...反而是多此一举虽然也能通,但ssh://gitlab-work/group/repo.git这种写法会把User覆盖掉,需要写成ssh://git@gitlab-work/group/repo.git才完整。
心得:同一个 GitLab 实例上,一把公钥只能绑定一个账号。如果出现"该密钥已被占用"的提示,不是系统坏了,而是这把钥匙已经挂在别的账号上了,要么换一把新生成的,要么去旧账号里删掉。
4. 日常使用中的实战配置
4.1 克隆、切换与 remote 地址的坑
配置完成后第一次克隆,用 SSH 形式的地址。GitLab 项目首页的克隆按钮里能直接切到 SSH 那一栏,复制出来大概是git@gitlab.example.com:group/subgroup/repo.git的形态。这个冒号写法是 SCP 风格的简写,等价于ssh://git@gitlab.example.com/group/subgroup/repo.git,两者都能用,但要注意冒号后面是相对路径,前面不能有多余的斜杠。
已经用 HTTPS 克隆过的仓库,不用重新克隆,改remote就行:
git remote -v # 先看清现状 git remote set-url origin git@gitlab.example.com:group/repo.git git remote -v # 再确认改对了切换过程中最常见的坑是子组路径丢失。比如仓库实际在group/subgroup/repo,有人只写了group/repo,git fetch会报Repository not found——这个报错有迷惑性,看着像权限问题,实际是路径错了。判断方法:用网页上的完整路径对照,一个字符一个字符比。另一个坑是大小写,路径部分在某些服务端配置下是大小写敏感的,别凭记忆手打。
4.2 agent 与口令的免交互管理
加了口令的私钥,每次用都要输一遍,这就是 agent 存在的意义。它把解密后的私钥放在内存里,会话期间复用。
eval "$(ssh-agent -s)" # 启动 agent,输出环境变量 ssh-add ~/.ssh/id_ed25519_gitlab # 加载私钥,此时输入一次口令 ssh-add -l # 列出已加载的密钥macOS 上可以把私钥存进系统钥匙串,重启终端甚至重启机器后依然有效:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_gitlab并在~/.ssh/config里加上:
Host * UseKeychain yes AddKeysToAgent yesLinux 桌面环境通常由桌面会话自动管理 agent,ssh-add一次能撑到注销。服务器环境要注意别在共享账号下加载私钥,agent 的 socket 如果权限不当,其他用户可能借你的身份操作。
ssh-add -D清空所有已加载密钥,ssh-add -d 路径删掉指定的一把。见过有人 agent 里堆了七八把密钥,每次连接都把服务端的尝试次数耗光,最后误判成"密钥失效"。定期清理是个好习惯。
4.3 流水线里的密钥:部署密钥与变量的用法差异
把密钥塞进 CI/CD 是另一套玩法,核心原则只有一条:绝不复用个人密钥。个人密钥代表你这个人,权限覆盖你所有仓库;流水线需要的是最小权限,只够拉取或推送某一个项目。
GitLab 提供了部署密钥机制,在项目设置的仓库选项里添加公钥时,可以勾选是否允许写权限。部署密钥默认只对当前项目生效,而且可以精确控制读写。生成时建议单独造一把,注释里写明用途:
ssh-keygen -t ed25519 -C "deploy-readonly-projectX" -f ./deploy_key -N ""-N ""表示空口令,这是流水线场景下少数可以接受留空的情况——因为密钥只存在于 CI 变量中,生命周期由平台管控,用完即弃。把私钥内容存进项目的 CI/CD 变量里,勾选"保护"和"掩码",只在受保护分支的流水线中可见。
流水线脚本里大致是这样用的:
mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 # 预先写入主机指纹,避免交互式确认 echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts git clone git@gitlab.example.com:group/repo.gitSSH_KNOWN_HOSTS建议存成变量而不是每次跑ssh-keyscan。后者在每次执行时都重新获取,一旦网络环境里出现异常,就可能把错误的指纹钉进去,属于把信任决策交给了运行时。存成变量等于把已验证的指纹固化下来,行为可预期。
部署密钥和个人密钥还有一个实际差异:部署密钥在 GitLab 的密钥列表里显示为独立的条目,能追踪到最近使用时间,到期后可以单独吊销而不影响任何人。这个可追溯性在多项目、多流水线的环境里价值很高。
5. 排错实录:报错信息逐条对照
5.1 Permission denied 的六步排查链路
git@gitlab.example.com: Permission denied (publickey).是出现频率最高的报错,字面意思是"服务端不接受任何提供的公钥"。它可能对应七八种不同的根因,盲目删文件重来是最低效的做法。我自己的排查顺序固定如下,一般三步内能定位。
第一步,加-v看客户端到底用了哪把密钥:
ssh -vT git@gitlab.example.com 2>&1 | grep -E "Offering|Server accepts|Authentications"输出里会有一行Offering public key: ...,后面跟着路径或指纹。如果看到的是~/.ssh/id_rsa而不是你新生成的 ed25519,说明客户端根本没找到正确的文件,问题出在IdentityFile或者文件名拼写上。如果看到Server accepts key:后面跟了内容,但最终仍然失败,那问题多半在私钥本身(口令没输对、文件损坏)。
第二步,确认公钥绑在了正确的账号上。同一个人有两个账号、两把密钥,很容易出现"公钥传到 A 账号,代码在 B 账号下"的情况。登录网页看一眼密钥列表的指纹,和本地ssh-keygen -lf的输出比对。
第三步,确认 agent 没有捣乱。ssh-add -l看看加载了哪些,如果有干扰项,先ssh-add -D清空再单独加载目标密钥测试。
第四步,看服务端是否根本没收到请求——端口不通时提示会不一样,通常是Connection timed out或Connection refused,这两种情况先换端口测试,别在密钥上纠缠。
第五步,检查~/.ssh/config里的Host匹配。别名写成了gitlab-work,但git remote里用的是gitlab.example.com,config 根本没被应用,IdentityFile也就没生效。ssh -G 主机名可以打印出最终生效的配置,排查别名匹配很有效。
第六步才是考虑重新生成。前面五步都没问题,那可能是密钥文件在传输中损坏了(比如从别的机器拷过来时被文本编辑器改了行尾)。对比一下公钥末尾的注释是否还在,ssh-keygen -y -f 私钥路径能从私钥推导公钥,跟.pub文件对一下就知道有没有坏。
5.2 常见报错速查表
排错最怕的是看到一个不认识的报错就慌了。下面这些是我这些年实际遇到并记录下来的,覆盖了绝大多数场景。
| 报错信息 | 根因 | 处理动作 |
|---|---|---|
Permission denied (publickey) | 公钥未绑定、密钥未加载或路径错 | 按上一节的六步链路走 |
WARNING: UNPROTECTED PRIVATE KEY FILE! | 私钥权限过宽 | chmod 600私钥文件 |
Host key verification failed | 主机指纹变化或已知主机记录过期 | ssh-keygen -R 主机名后重连确认 |
Too many authentication failures | agent 里密钥过多,尝试次数超限 | config 加IdentitiesOnly yes |
sign_and_send_pubkey: signing failed | agent 未加载或拒绝操作 | ssh-add 私钥路径重新加载 |
Key has already been taken | 同一把公钥已绑定其他账号 | 生成新密钥或解绑旧账号 |
Fingerprint cannot be generated | 粘贴内容不是合法公钥 | 检查是否误贴私钥或格式损坏 |
Repository not found | 路径错误或账户无权限 | 核对完整路径与账号身份 |
Connection timed out | 端口被拦或域名解析异常 | 换 2222 备用端口测试 |
PTY allocation request failed | 未加-T参数 | 用ssh -T验证,属正常提示 |
表格能覆盖九成情况,剩下那一成往往藏得很深。我遇到过一次:所有配置都对,本地测试也通,但流水线里就是失败。最后发现是 CI 镜像里有一个GIT_SSH_COMMAND环境变量被预设,指向了一个带额外参数的包装脚本,把IdentityFile覆盖掉了。这类问题的教训是——当你确信本地配置无误,就去看环境变量和包装脚本,env | grep -i ssh和git config --list两把刷子先过一遍。
5.3 调试开关与日志的正确打开方式
ssh -v是入门,-vvv是终极手段。三级详细模式会打印出密钥交换、算法协商、每个认证方法的尝试过程,信息量大到刷屏,但定位问题时非常有效。我的习惯是配合 grep 过滤:
ssh -vvvT git@gitlab.example.com 2>&1 | grep -iE "identity|offer|accept|auth|error"重点看三处:一是identity file列表,确认客户端按什么顺序找了哪些文件;二是Offering那几行,看实际发出去的是哪把公钥;三是结尾的Authentications that can continue,它会列出服务端还接受哪些方法,如果只剩publickey而你的公钥被拒,基本可以锁定是绑定关系问题。
Git 层面的调试用GIT_SSH_COMMAND环境变量临时注入参数,不用改 config:
GIT_SSH_COMMAND="ssh -vvv" git clone git@gitlab.example.com:group/repo.git这个技巧在排查"为什么 git 用的密钥和命令行不一样"时特别好使,因为它会把 git 实际调用的 SSH 命令原样暴露出来。我还用它做过一次验证:某台机器上 git 走的是系统自带的旧版 SSH,而命令行里ssh是新版的,两者读的 config 路径不同,导致行为不一致。
提示:调试输出里会包含密钥指纹、路径、用户名等信息,贴到公开渠道之前先扫一遍,把私钥内容(正常情况下不会出现)和内部域名清理掉再发。
6. 安全习惯与团队约定
6.1 密钥轮换与撤销的时机
密钥不是配一次管一辈子的东西。我给自己的规则是:换设备、换岗位、发现异常、以及每年固定时间点,这四种情况触发轮换。轮换流程其实就是"新生成一把 → 上传 → 验证可用 → 删掉旧的",中间有一段并存期,保证不中断。
撤销要及时。GitLab 的密钥列表里能看到每把钥匙的创建时间和最近使用时间,"最近使用"这一列很有价值——如果一把半年前就该停用的密钥还在显示近期有活动,那要么是旧设备还在跑什么自动化,要么就是有问题,得查。离职流程里,账号回收之外,专门有人核对一次 SSH 密钥和访问令牌,这是基本操作。
口令管理也值得说一句。私钥口令别用你常用的那几个密码,最好是密码管理器生成的随机串,存在管理器里。有人担心管理器挂了怎么办——那就把恢复码打印一份收好,比把口令写在便签贴显示器上强。
6.2 团队场景下的几条硬约定
团队里密钥配置混乱,往往不是技术问题,而是缺少约定。我们内部沉淀了几条,执行下来效果不错。
第一条,密钥命名必须带用途和设备标识。work-laptop-2024、deploy-projectX-readonly,一眼能看懂,审计时不用挨个问人。第二条,禁止在共享机器上生成个人密钥,需要在那类环境操作就申请专用账号和专用密钥,用完即删。第三条,流水线一律使用部署密钥,绝不把个人私钥塞进 CI 变量,这条是红线,因为个人密钥的权限范围是整个账号,出事就是连坐。
第四条,公钥可以随便传,私钥绝不外发,包括不发给自己。需要备份的话,用加密方式存起来,别裸放同步盘。第五条,任何人发现某个项目里有疑似泄露的私钥,第一反应是通知管理员吊销,而不是自己删文件了事——吊销是让密钥失效,删文件只是眼不见心不烦。
7. 几个让我印象深刻的现场
说两个真实踩过的坑,比干讲原理有用。
一次是团队新人配置完,ssh -T测试通过,但git clone就失败。前后折腾了四十分钟,最后发现他用的是 Windows 的 Git Bash,HOME环境变量指向了C:\Users\xxx,而 git 认的是%USERPROFILE%,两个路径下各有一份.ssh目录,密钥装在了其中一个里面。命令行ssh用 A 目录,git 调用时用 B 目录,行为完全分裂。解决办法是把两份统一,或者显式设置GIT_SSH_COMMAND指向正确路径。Windows 上的路径问题还有一类:用户名带空格或中文,某些工具解析时截断,表现出的症状也是"密钥找不到"。
另一次更隐蔽:某台构建机上所有配置看起来都对,ssh -T能通过,但 clone 大仓库时随机失败,报认证错误。反复出现了两三天才定位到——那台机器上同时跑着多个任务,ssh-agent的 socket 被并发写坏了,连接在密钥认证阶段偶发丢失。换用独立的 agent 实例(每个任务单独eval $(ssh-agent -s))之后问题消失。这类问题的特征是"时好时坏",而密钥配置类问题通常是"一直坏",用这个规律可以快速分流。
我在实际使用中最大的体会是:SSH 配置这件事,80% 的时间花在排错上,而排错效率取决于你有没有建立清晰的排查顺序。命令就那么几条,ssh -vT、ssh-add -l、ssh-keygen -lf,但知道先敲哪一条、看输出里的哪一行,才是区分"折腾一下午"和"三分钟解决"的关键。下次再遇到Permission denied,别急着重装,先把-v的输出读一遍,答案通常就在里面。