GitLab SSH 密钥配置全链路:从生成、多账号到 Permission denied 排错
上周帮一位开发者排查 clone 失败的问题终端里反复就一行Permission denied (publickey)而他很确定公钥已经贴进 GitLab 后台三遍了。等我把他机器上的~/.ssh目录翻了一遍才发现问题根本不在公钥而是本地同时存着四把私钥、agent 里塞了三把缓存、config文件里又写了个IdentityFile指向早已删掉的旧路径。GitLab 的 SSH 密钥配置这件事生成密钥、粘贴公钥只是表面动作真正决定成败的是文件命名、权限、agent 加载顺序、config匹配规则这一整套链路。这篇就把这条链路从生成到排错完整拆一遍为什么 ed25519 比 RSA 更值得选、多账号共存时config该怎么写、ssh -T返回什么才算真的通了、遇到Permission denied又该按什么顺序去定位。适合刚接触代码托管平台的新手也适合那些配过很多次但从没搞清楚原理、每次出问题只能靠删掉重来的老手。1. 密钥认证真正在做什么以及它为什么比密码更值得配1.1 HTTPS 与 SSH 两条路径的本质差别大多数人第一次 clone 仓库用的是 HTTPS 地址输入账号密码或者令牌就完事了看起来很顺。但这条路径的每次推送都要把凭据交给远端校验凭据一旦泄漏别人拿到的是能操作你仓库的通行证而且凭据有有效期过期之后就是满屏的认证失败提示。SSH 走的是另一条逻辑本地留一把私钥远端只存对应的公钥握手时双方做一次数学验证私钥全程不出本机。换句话说HTTPS 是每次出示身份证让别人核对SSH 是用一把只有你有的钥匙开一把只有你能开的锁。这个差别在实操中会放大成两个具体收益。一个是免交互配置好之后git push直接走不会再弹任何输入框另一个是适合自动化构建任务、部署脚本、定时同步这些场景没法手动输密码SSH 或者部署密钥几乎是唯一干净的解法。代价是密钥本身变成了需要保管的资产——私钥泄漏等于仓库权限泄漏而且它不像密码那样容易被重置撤掉公钥要上后台手动删。1.2 握手阶段 GitLab 到底校验了什么很多人以为公钥上传了就等于认证通过了其实链路比这复杂。完整的握手大致是这样客户端先连上服务端服务端从自己维护的密钥列表里挑出所有能匹配的身份向客户端发起质询客户端用本地私钥对质询内容签名把签名连同公钥指纹一起回传服务端拿之前存下的公钥验证签名验证通过才认这个用户。这里面有一个关键点服务端是靠公钥指纹来匹配用户身份的所以它必须能在一堆已上传的公钥里找到与你提供的私钥配对的那一把。这就解释了为什么很多我明明上传了的故障会产生。你的私钥可能不止一把客户端可能先拿了一把不在列表里的去试服务端拒绝后客户端又换下一把试到第六把的时候服务端直接掐断连接客户端最后只吐出一句含糊的Permission denied。所以后面讲排错时我会反复强调一个动作先确认客户端实际用了哪把私钥去握手而不是急着怀疑远端没保存。1.3 三类最容易出问题的使用场景把常见情况归归类出问题的场景基本就三种。第一种是单机单账号但配过多次历史遗留的旧密钥、known_hosts里的过期记录、路径写错的config混在一起现象是以前能用现在不行。第二种是一机多账号个人账号和公司账号共用一台笔记本两把密钥互相抢位clone 下来发现提交人是另一个身份。第三种是非交互环境比如构建流水线里用部署密钥这种环境没有 agent、没有桌面提示符任何一个权限位或路径写错都会静默失败。这三种场景的解法并不相同往下我按生成—上传—多账号—排错—自动化的顺序走你可以对着自己的情况直接跳章节。2. 生成密钥对算法、文件名、口令这三件事必须先想清楚2.1 ed25519 与 RSA 的取舍不是玄学密钥生成的命令就那么一条但参数选错会在半年后某个老系统上炸掉。当前主流选择是 Ed25519 和 RSA我的默认建议是 Ed25519只有在需要对接一些版本很旧的客户端或设备时才退回 RSA 4096。算法生成参数公钥长度兼容性适用判断Ed25519-t ed25519约 68 字符单行较新客户端全支持首选签名快、密钥短RSA-t rsa -b 4096很长折行显示极广老系统也认兜底方案兼容优先ECDSA-t ecdsa -b 521中等部分环境不认一般不主动选DSA-t dsa短已被大量平台禁用不要用为什么优先 Ed25519一是同等安全强度下密钥短得多粘贴到后台不会因为换行被截断二是签名验证的开销更低高频操作时体验更顺三是它在设计上避开了某些曲线参数的可信度争议。而 RSA 的优势只有一个——老但老在某些内网设备、老旧构建镜像面前就是硬通货所以我的做法是主用 Ed25519同时保留一把 RSA 4096 备用遇到不认的对方就切过去。生成命令长这样ssh-keygen -t ed25519 -C work-laptop-2024 -f ~/.ssh/id_ed25519_gitlab这里的-C是注释会原样附加在公钥末尾看起来只是个备注实际上它的价值在于你半年后能一眼认出这把钥匙是给哪台机器、哪个用途配的。我见过太多人三把密钥全叫id_ed25519最后完全分不清哪个对应哪个账号。注释建议写成设备-用途-年份这种能自解释的格式别写mykey这种等于没写的内容。-f指定私钥的保存路径生成的私钥就是这个名字公钥会自动加.pub后缀。这里有个新手常犯的错误不写-f直接回车然后默认路径被覆盖把之前能用的密钥顶掉了。覆盖是不可逆的除非你还留着公钥能反推其实反推不了所以生成前先ls ~/.ssh看一眼现状这个动作花两秒钟能省掉一次事故。2.2 多密钥共存时的命名策略一台机器上有多把密钥是常态不是例外。命名我一般遵循用途在前、设备在中的规则比如id_ed25519_personal—— 个人账号id_ed25519_work—— 工作账号id_rsa_legacy—— 留给老系统的兜底这样一眼就能看出该用哪把也方便在config文件里引用。不要用key1、key2这种时间一长你自己都记不住。另外把公钥统一列出来看一眼是个好习惯ls -l ~/.ssh/*.pub ssh-keygen -lf ~/.ssh/id_ed25519_gitlab.pub第二条命令会输出密钥的位数和 SHA256 指纹。这个指纹非常有用——GitLab 后台在密钥列表里也会显示对应的指纹两边对一下就知道我手里这把和后台存的那把是不是同一把。这一步能直接排除掉上传了但传错文件这类低级但高频的问题。还有一个反向操作值得记住从私钥反推公钥。当你手里只有私钥、不确定它对应哪把公钥时ssh-keygen -y -f ~/.ssh/id_ed25519_gitlab输出的就是配套公钥内容。这个技巧在接手别人机器、或者清理历史密钥时特别好使。2.3 passphrase 要不要设设了怎么不烦passphrase 是给私钥再加一层口令保护。设了的好处很实在私钥文件被拷走也用不了尤其是笔记本丢失或者多人共用的开发机上这一层几乎是必须的。坏处是每次使用都要输一次破坏免交互体验。解法是用 agent 缓存。启动 agent 并加载私钥eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519_gitlab ssh-add -lssh-add -l用来确认当前 agent 里装了哪些密钥排查问题时这条命令的价值极高——很多怎么都用不对的密钥就是因为 agent 里缓存了一把老的客户端优先用了它。macOS 上可以把密钥存进系统钥匙串省去每次重启后重新添加ssh-add --apple-use-keychain ~/.ssh/id_ed25519_gitlab再在config里加上AddKeysToAgent yes和UseKeychain yes体验基本等于无感。Windows 的话在 Git Bash 里同样能用ssh-agent但要注意不同终端会话之间的 agent 不共享PowerShell 里加过的密钥在 Git Bash 里未必可见这个坑后面排错章节还会提。我的取舍是个人机器上给私钥设 passphrase 并用 agent 缓存一次性配置长期受益纯自动化环境用专属的无口令密钥但这类密钥必须限制权限范围比如只读部署密钥并且单独一把、不复用。2.4 权限位不是形式主义是硬性检查SSH 客户端在加载私钥前会检查文件权限如果私钥对同组或其他用户可读直接拒绝使用并报UNPROTECTED PRIVATE KEY FILE。这不是啰嗦是因为私钥泄漏的后果太严重。标准权限是chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519_gitlab chmod 644 ~/.ssh/id_ed25519_gitlab.pub chmod 600 ~/.ssh/config公钥可以宽松644因为它本来就是公开的私钥和config必须 600目录必须 700。Windows 上 NTFS 权限模型不同一般不会触发这个检查但在 WSL 里访问挂载的 Windows 目录时权限会显示成 777这时候 SSH 会直接报错拒绝加载。遇到这种情况的处理方式是把密钥复制到 WSL 自己的家目录下而不是去改挂载权限——后者往往改不动。3. 从本地公钥到后台记录上传环节的几个细节3.1 复制公钥的正确姿势公钥就是那个.pub文件的内容一整行以ssh-ed25519或ssh-rsa开头结尾是注释。复制时最容易出的问题是多复制了换行或少了字符尤其是公钥很长需要折行显示的时候从终端里手动选中很容易漏掉尾部。稳妥的做法是用命令直接输出# macOS pbcopy ~/.ssh/id_ed25519_gitlab.pub # Linux xclip -sel clip ~/.ssh/id_ed25519_gitlab.pub # Windows PowerShell Get-Content ~/.ssh/id_ed25519_gitlab.pub | Set-Clipboard没有剪贴板工具就cat出来手动复制但要确认复制的是以ssh-开头、以注释结尾的完整一行。如果后台提示密钥格式无效九成是复制不完整或者中间夹了换行。注意不要复制私钥内容没有.pub后缀那个文件。私钥是-----BEGIN OPENSSH PRIVATE KEY-----开头的一大段多行文本和公钥长得完全不同。把自己的私钥贴进任何网页都是严重事故。3.2 后台添加时容易被忽略的两个字段在个人设置里的 SSH 密钥页面粘贴时除了密钥内容本身还有两个字段值得认真填。标题这个字段很多人随手写laptop结果半年后列表里五个 laptop不知道该删哪个。建议和本地注释保持一致写清设备与用途比如办公室台式-主账号。过期日期字段默认可能留空表示永久有效。这在个人主力机上没太大问题但如果是临时设备、共享机器、或者给外部协作者开的临时权限建议设一个明确期限。密钥不像口令那样容易被发现泄漏它会静默地一直有效下去设过期时间本质上是给自己一个必须回来复查的提醒。还有一个容易混淆的点部署密钥和用户密钥是两套东西。用户密钥绑定到账号能访问你所有有权限的仓库部署密钥绑定到单个项目通常只读适合构建环境。不要为了给流水线用就把自己的账号密钥塞进去权限过大且难以回收。3.3 首次连接为什么要核对指纹第一次连接一个新主机时客户端会打印对方的主机密钥指纹并问你是否信任。很多人习惯性敲 yes这个动作本身没错但前提是你确认了对方是谁。主机密钥的作用是防中间人它证明我连的这个服务端确实是我以为的那个服务端。指纹核对的方式有几种——官方文档通常会公布自己实例的指纹如果是自建实例可以直接登录服务器看ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub比对一致再接受。这个习惯在公共网络环境下尤其值得养成因为SSH 协议本身的加密不解决对端是不是真的这个问题主机密钥才是那一环。接受之后对方的主机密钥会被写进~/.ssh/known_hosts。后续如果服务端换了主机密钥重装、迁移、负载均衡后端不一致客户端会直接报Host key verification failed并拒绝连接。这不是 bug是设计如此。3.4 验证闭环什么样的返回才算真通了上传完公钥别急着 clone先做一次纯认证测试ssh -T gitgitlab.com-T表示不分配伪终端因为 git 服务本来就不提供交互 shell。成功的返回通常是这样一句Welcome to GitLab, your-username!看到这句说明公钥匹配、账号识别、权限链路全部打通。如果返回Permission denied (publickey)说明认证没通过如果卡住很久最后超时那是网络层或端口的问题跟密钥无关。这三种返回对应三个完全不同的排查方向先把它们区分开能省掉大量瞎试。有一个细节值得说这个测试只验证认证不验证仓库权限。认证通过但访问某个仓库报 404 或拒绝那是项目成员权限的问题不是密钥的问题。把这两层分开看思路会清楚很多。4. 一机多账号config 文件才是真正的分水岭4.1 Host 别名怎么写、怎么匹配单账号用户改完前面几步基本就结束了。真正需要动脑子的是多账号场景同一台机器一个个人账号、一个工作账号两边都配了 SSH怎么让 git 知道这次该用哪把钥匙答案是~/.ssh/config里的 Host 别名。基本写法Host gitlab-work HostName gitlab.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes Host gitlab-personal HostName gitlab.com User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes关键点逐个说。Host是你自己起的别名后面 clone 时就用它代替真实域名HostName才是真实地址User git是代码托管服务的固定约定不要写成你的账号名——这一点新手极易搞错写成自己的用户名会导致认证直接失败。IdentityFile指定这把别名专用哪把私钥。IdentitiesOnly yes这一行我强烈建议加上它的作用是只使用配置里显式指定的密钥不去挨个尝试 agent 里缓存的其它密钥。没有这一行客户端会先把 agent 里的密钥全部试一遍试到服务端的最大尝试次数就断开报出来的错误是Too many authentication failures和真正的密钥不匹配完全是两回事特别容易误导排查方向。匹配优先级上config是按顺序从上往下匹配的第一个命中的Host生效。所以如果有通配写法比如Host *一定要放在文件最末尾否则会把后面所有具体配置都吃掉。4.2 clone 地址必须和别名对齐配置写好了clone 的时候要把地址里的域名换成别名git clone gitgitlab-work:group/project.git而不是gitgitlab.com:group/project.git。这两者的区别在于前者会命中config里gitlab-work那段配置用指定的私钥后者走默认逻辑可能用错密钥。已经 clone 下来的仓库改地址git remote set-url origin gitgitlab-work:group/project.git git remote -v这一步是很多配了 config 但还是不对的根因——配置改对了但仓库的 remote 还指着旧地址等于配置没生效。4.3 agent 里的密钥顺序也会影响结果即使不加IdentitiesOnlyagent 里密钥的加载顺序也会影响命中结果。查看当前加载了哪些ssh-add -l输出会列出每把密钥的指纹和路径。如果发现里面混着不该出现的旧密钥清掉ssh-add -D # 清空全部 ssh-add ~/.ssh/id_ed25519_work我的习惯是agent 里只保留当前正在用的密钥需要切换时清空重加。这比让 agent 里堆五六把、指望客户端自己选要可靠得多。也可以用ssh-add -d 路径单独移除一把。4.4 提交身份和认证身份是两件事这里有个非常经典的混淆SSH 密钥决定的是你有没有权限推代码而user.name/user.email决定的是这次提交署谁的名。这两者完全独立。一机多账号时常见事故是用工作密钥推了代码但user.email还是个人的结果提交记录里显示的是个人邮箱。解决办法是在仓库级别单独设git config user.name 工作用名 git config user.email workexample.com或者在config里用条件包含较新版本的 git 支持[includeIf gitdir:~/work/] path ~/.gitconfig-work把工作相关的仓库统一放在一个目录下进这个目录就自动切换提交身份。这个技巧能省掉每次手动git config的麻烦也避免漏配。5. 排错链路从一个报错倒推到具体位置5.1 Permission denied 的六种成因同样是Permission denied (publickey)背后的原因可能完全不同。我按出现频率排了个序遇到问题从上往下查命中率最高。序号成因快速验证方式1私钥没被加载agent 里是空的ssh-add -l看列表2私钥路径写错或文件被删ls -l检查路径是否存在3公钥没真正上传或传了另一把后台看指纹与ssh-keygen -lf对比4私钥权限过宽被拒绝加载看详细日志里是否有 UNPROTECTED 提示5agent 里密钥太多超过尝试上限报错里出现 Too many authentication failures6config里 User 写成了账号名检查是否写成User git排查顺序我一般是先ssh-add -l确认加载状态再ssh-keygen -lf比对指纹然后看config内容最后才怀疑远端。不要一上来就删后台公钥重传那个动作会把本来正确的配置也打乱反而增加变量。5.2 用 -vT 把握手过程摊开看单看一句报错几乎是没办法定位的必须让客户端把过程吐出来。三个详细级别ssh -T gitgitlab.com # 只报结果 ssh -vT gitgitlab.com # 基本信息够用 ssh -vvT gitgitlab.com # 详细信息 ssh -vvvT gitgitlab.com # 最详细含密钥交换细节从-vT的输出里重点看三类行Offering public key: ...后面跟的路径或指纹 —— 这是客户端实际尝试的密钥和你以为的未必一样Server accepts key:—— 说明这把被接受了问题在别处Authentications that can continue: publickey—— 服务端允许的方式如果这里没有publickey说明服务端根本不支持密钥认证配置问题不是你的问题。如果输出里连续出现好几个Offering public key然后以失败告终基本可以确定是密钥太多或者顺序不对加IdentitiesOnly yes就能解决。如果一把都没 offer那就是密钥压根没被加载回头查 agent 和config。5.3 端口不通和密钥无关有一类现象特别容易被误判成密钥问题命令挂住十几秒然后超时报Connection timed out或Connection refused。这跟密钥一点关系都没有是网络层或者端口层的问题。判断方法很简单ssh -vT gitgitlab.com如果日志停在Connecting to ...之后就没有下文了那就是连不上如果能走到密钥交换阶段才失败才是认证问题。某些内网环境只放行 443 端口22 端口出不去。这种情况下部分平台提供了备用端口的 SSH 接入方式写法是在config里指定PortHost gitlab-work HostName gitlab.com User git Port 443 IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes具体端口的支持情况要看平台的官方文档别凭猜测填。判断端口通不通也可以直接用 telnet 类工具测先确认链路通再去折腾密钥。5.4 known_hosts 冲突的处理Host key verification failed这个报错字面意思是对方的主机密钥和我记录的不一样。常见触发场景服务端做了迁移或重装、用了多台后端机器导致指纹不一致、或者你之前手动改过known_hosts。处理方式是删掉旧记录重新握手ssh-keygen -R gitlab.com-R会从known_hosts里移除指定主机的所有记录然后下次连接时会重新询问你是否信任。删之前最好确认一下这确实是预期内的变更而不是真的有人在中间做手脚——虽然绝大多数情况是运维操作导致的但绝大多数不等于全部。如果只是想临时跳过检查不建议长期这么干可以用-o StrictHostKeyCheckingno但这等于放弃了中间人防护只在一次性调试时用。5.5 平台差异带来的坑Windows上有两套 SSH 客户端容易打架系统自带的 OpenSSH 和 Git for Windows 附带的那个。它们读的配置路径可能不同agent 也不共享。表现就是命令行里ssh -T通了但git clone报认证失败。定位方法是让 git 显式指定用哪个GIT_SSH_COMMANDssh -vT git clone gitgitlab-work:group/project.git日志里能看到 git 实际调用的是哪个 ssh 可执行文件。要固定下来可以设core.sshCommand配置项。WSL与 Windows 宿主的家目录是分开的密钥不共享。在 Windows 上配好的密钥WSL 里ls ~/.ssh是空的。要么重新生成一把要么把密钥复制过去并重新设权限注意复制过去之后权限会变成 777必须chmod 600。macOS上主要是钥匙串的持久化问题前面提过加UseKeychain yes和AddKeysToAgent yes可以解决重启后需要重新ssh-add的烦恼。另外 macOS 从某个版本开始默认不再自动加载~/.ssh/id_rsa必须显式写进config这也是以前能用升级后不行的常见原因。6. 把密钥用在自动化环境里边界要划清楚6.1 部署密钥与账号密钥的权限差异构建流水线、部署脚本这类场景第一原则是不要复用个人账号密钥。个人密钥绑账号权限覆盖你有权访问的所有仓库一旦泄漏影响面是整个账号而部署密钥绑单个项目通常只读泄漏了损失可控。配置入口在项目的仓库设置里添加时可以选择是否允许写权限只拉代码的流水线就只给读。生成部署密钥时用独立的一把别和本地开发用的混在一起ssh-keygen -t ed25519 -C ci-deploy-projectX -f ~/.ssh/id_ed25519_ci_deploy -N -N 表示空口令因为流水线环境没有交互无法输入 passphrase。空口令意味着这把私钥必须被严格保管通常放在流水线的密钥变量里并且开启受保护分支才可用这类限制避免在任意分支的构建中都能读到。6.2 流水线里最容易静默失败的三处自动化环境和本地最大的不同是没有交互提示出错时往往只给一句含糊的失败。三个高频点第一是主机指纹未预置。首次连接会询问是否信任非交互环境直接失败。解决办法是提前把主机公钥写进known_hosts或者用平台公布的指纹做校验后写入而不是简单粗暴地关掉检查。第二是私钥文件权限。流水线里从变量还原出来的私钥文件权限通常是 644 或 777SSH 会拒绝加载。加一步chmod 600就好但这一步漏了就会得到和密钥不正确一模一样的报错。第三是换行被破坏。私钥是多行文本存在变量里时首尾换行、末尾空行经常在处理过程中被裁掉还原出来的文件格式不合法。稳妥做法是存储时保留完整内容还原后用ssh-keygen -y -f试一下能不能正常解析解析失败就说明内容坏了。这一步花一秒钟比在流水线里瞎猜半小时强得多。6.3 定期清理比配好更重要最后说个容易被忽略的事密钥是会过期的资产需要定期盘点。我的习惯是每季度做一次清理检查三件事。本地~/.ssh目录里有哪些密钥、对应哪些平台、还有没有在用后台账号的密钥列表里哪些是已经换掉的旧设备、哪些设置了过期时间流水线里引用的部署密钥对应项目是否还在维护。清理时先删后台记录再删本地文件顺序反过来会导致中间状态无法验证。删之前用指纹核对一遍确认删的确实是要删的那把。这件事听起来琐碎但真出过一次离职同事的密钥还在项目里有写权限的情况之后就会发现这个习惯比任何一次排错都值钱。配置 SSH 这件事本身不难难的是它涉及的环节多——算法、路径、权限、配置文件、agent、远端记录任何一环错了报错信息都长得差不多。我自己的经验是每次改完配置先跑ssh -T通了再去 clone出问题时先看-vT日志里到底用了哪把密钥而不是先怀疑远端。把这两条固定成习惯绝大多数坑都能在五分钟内定位到。