
凌晨一点半代码总算跑通你顺手敲下git push终端却只甩回来一行红字gitgitee.com: Permission denied (publickey). Could not read from remote repository.第一反应几乎所有人都是去确认密码——是不是我记错了是不是账号被锁了。可你会发现无论重输几次密码这行字还是原封不动地蹲在那里因为这条报错从头到尾跟密码没有半点关系。它说的是另一件事你递过去的那把钥匙对方不认。这篇东西就是围绕gitgitee.com: Permission denied (publickey)这条报错展开的一整套处置流程从这行字里每个词的准确含义到本地该怎么生成一把 Gitee 能认的密钥再到密钥明明配好了却依旧报错时该怎么一层层把根因揪出来。刚装完 git、第一次往 Gitee 推代码的新手能直接照着做换电脑、重装系统之后被这个问题绊住的老手也能从排查链路那一节里找到省时间的东西。命令、路径、权限位我都会尽量写全Windows 和 macOS / Linux 的差异单独拎出来说因为同一条报错在不同系统上成因往往差得很远。1. 报错的五个词其实已经把答案说了一半1.1 把这一行字拆开读很多人看到报错只会扫一眼「Permission denied」就开始慌其实这行字的信息量比想象中大得多逐段读一遍能省掉大量试错。gitgitee.com是目标地址格式是「用户名主机名」。这里用户名的位置写的是git注意它不是你的 Gitee 账号名而是 Gitee 给所有 SSH 访问者准备的一个公共账号身份谁连上来都是git。所以如果你看到有人把这里的git换成自己的用户名去试那是白费力气。Permission denied是服务端给出的拒绝结论直译就是权限不足或者不接受。注意主体是服务端不是你本地本地这边能做的事情已经做完了是对方在最后一步把门关上了。括号里的(publickey)则是整条报错里最关键的一个词。SSH 连接时可以提出好几种认证方式——密码、键盘交互、公钥等等——括号里写什么就代表服务端最后指望你走哪一条路而你在这条路上失败了。它写的是publickey说明这次连接压根没走到密码那一环纯粹是密钥这条路没走通。Could not read from remote repository是一句通用兜底文案原话后面通常还跟着「Please make sure you have the correct access rights and the repository exists」。这句话最容易误导人因为它会让人以为是自己把仓库地址写错了、或者仓库压根不存在。绝大多数情况下仓库地址是对的仓库也确实存在只是认证没过git 只能给出这么一句模糊的总结。1.2 公钥认证里其实有三个角色想彻底搞明白这条报错得先弄清楚 SSH 免密登录到底在验什么。我用一个生活化的场景来类比一扇门、一把锁、一份住户名册。你本地电脑这一侧.ssh目录下躺着一对文件一个叫id_ed25519或者id_rsa这是私钥相当于你手里那把实体钥匙另一个叫id_ed25519.pub这是公钥相当于你把钥匙的形状拓印下来做成一份可以随便公开的图纸。私钥从不离开你的机器公钥则可以贴到任何一个平台上去。Gitee 那一侧你账号的 SSH 公钥列表就是那份住户名册。每次你用 SSH 连接Gitee 会先扔过来一段随机字符串你的客户端拿本地私钥对它做一次签名再把签名和公钥一起送回去Gitee 用名册里存的那份公钥去验这个签名对不对。验过了门开名册里翻不到这份公钥就是Permission denied (publickey)。理解了这三方关系很多困惑就自动消解了。比如「为什么我在网页上能正常登录 Gitee本地却推不上去」——因为网页登录走的是账号密码而 git 走的是 SSH 时Gitee 根本不看你的账号密码它只认那把钥匙。再比如「为什么我把 Gitee 密码改了之后突然不能推代码了」——密码和 SSH 是两条完全独立的通道改密码理论上不该影响 SSH真正会误伤的通常是你在 Gitee 上手动删掉或者替换了那把旧公钥。1.3 为什么平台偏爱公钥而不是密码有人会问直接让我输账号密码不是更省事吗为什么非要用密钥这套看起来麻烦的东西。站在平台的角度看公钥认证有几个密码替代不了的好处。一是可撤销。你在公司电脑、家里台式机、笔记本上各生成一把钥匙哪天笔记本丢了去 Gitee 上把对应那把公钥删掉就行其他两台机器完全不受影响。换成密码的话你就得改密码然后所有设备重新登一遍。二是不传密钥本体。整个握手过程中私钥不会以任何形式发到网络上服务端拿到的只是签名结果就算通信全程被抓包也拼不出你的私钥。三是适合自动化。CI 流水线、脚本部署这类场景里没人坐在那儿手输密码一把权限受控的部署密钥就能解决问题。Gitee 其实同时支持 HTTPS 和 SSH 两条路。HTTPS 走的是账号密码或者访问令牌SSH 走的就是公钥。这次报错里明晃晃写着publickey基本可以直接锁定你当前的这个远程地址走的是 SSH 通道。2. 动手之前先确认自己在走哪条路2.1git remote -v是第一诊断命令很多人一看到报错就开始生成密钥结果折腾半天发现方向根本不对。养成一个习惯遇到任何 git 连接类报错第一件事是在项目目录下执行git remote -v看远程地址到底长什么样。git remote -v # 输出示例一 origin gitgitee.com:yourname/yourrepo.git (fetch) origin gitgitee.com:yourname/yourrepo.git (push) # 输出示例二 origin https://gitee.com/yourname/yourrepo.git (fetch) origin https://gitee.com/yourname/yourrepo.git (push)区分方法很简单地址以git开头、冒号分隔用户名和路径的就是 SSH以https://开头的是 HTTPS。这两个协议背后是两套完全独立的认证体系报错信息也完全不一样搞混了就会在错误的方向上使劲。2.2 HTTPS 和 SSH 报错长得完全不同把两种协议的典型报错放在一起对比你就能一眼判断自己属于哪一类。协议典型报错报错关键词SSHgitgitee.com: Permission denied (publickey)publickey、Permission deniedHTTPSAuthentication failed/remote: HTTP Basic: Access denied/fatal: could not read UsernameAuthentication、Access denied、Username看到publickey三个字就不要再去找密码相关的东西了方向一定在密钥上。反过来如果你的报错是Authentication failed或者弹出一个要求输用户名密码的提示框那问题在 HTTPS 这边跟本文这一套处理流程基本无关该去检查的是访问令牌是否过期、凭据管理器里是不是存了旧的账号。2.3 该换协议还是该修密钥确认自己在走 SSH 之后还有个决策要做是老老实实修密钥还是干脆切回 HTTPS 省事。我个人对这件事的看法比较明确——个人开发环境推荐用 SSH一次性配好之后长期免密体验最好。但要分情况如果你只是临时在别人电脑上克隆一个仓库或者在一个受限的网络环境里比如某些企业办公网会限制 22 端口切回 HTTPS 反而更快。两者的取舍可以参照下表维度SSHHTTPS首次配置成本需要生成密钥、上传公钥基本为零输密码即可后续使用完全免密每次可能要输凭据或依赖凭据管理器端口默认 22443受限网络下可能被端口策略挡住通常通畅自动化场景部署密钥更干净需要管理令牌如果你确定要留在 SSH 这条路上但确实怀疑是网络端口的问题Gitee 也提供了备用端口的连接方式具体可以查它官方帮助里的说明这里不展开。绝大多数家庭网络和普通宽带下22 端口都是通的。提示切换远程地址不用重建仓库一条命令就能改。git remote set-url origin gitgitee.com:yourname/yourrepo.git把地址换成 SSH 形式或者换成 HTTPS 形式改完之后再用git remote -v确认一次即可。3. 生成一对密钥每一步都在问你什么3.1ssh-keygen交互细节确认要走 SSH 之后第一步是在本地生成密钥。现代系统上推荐这样写ssh-keygen -t ed25519 -C 你的邮箱或备注回车之后会出现三段交互很多人是闭着眼睛一路回车的但每一段其实都值得想一下。第一段问的是保存路径提示类似Enter file in which to save the key (/home/you/.ssh/id_ed25519):。建议直接回车用默认路径。原因很实际ssh 客户端在默认情况下会自动去~/.ssh目录下按固定名字找密钥文件你如果随手改成一个奇怪的名字或者放到别的目录接下来就必须靠配置文件手动指定等于给自己挖坑。除非你有明确的「一台机器上管理多把密钥」的需求否则不要动它。如果这个路径下已经存在同名文件它会问你是否覆盖这时候一定要看清楚——覆盖意味着旧密钥直接作废用它绑定的那几个平台全都要重新配置。第二段是设置口令也就是passphrase提示是Enter passphrase (empty for no passphrase):。这里有个取舍留空的话以后用这把钥匙完全不需要输入任何东西方便但安全性略低设置口令的话每次使用私钥都要解锁安全但麻烦。对于个人开发机我一般建议留空因为配合系统登录密码和磁盘加密风险是可控的如果你是在多人共用的机器上那就一定要设口令。设了口令之后如果嫌烦可以用ssh-add把它加载到内存里的密钥管理中一段时间内免输。第三段是确认口令直接再输一遍用空口令的话再回车一次就完事了。至于-C后面那串东西它只是注释会被原样写进公钥的末尾。它的唯一用途是当你在 Gitee 上挂了多把公钥时能通过这个备注快速分辨哪把是哪台机器的。写成邮箱、写成「工作笔记本」都行不影响任何功能。3.2 选 ed25519 还是 rsa算法选型是另一个经常被忽略的点。早年教程清一色教ssh-keygen -t rsa -b 4096现在更推荐ed25519。算法密钥长度写法特点适用场景ed25519固定长度无需 -b密钥短、生成快、安全性好现代系统首选rsa一般用-b 4096兼容性极好各种老系统都认老旧服务器、特殊设备ecdsa-b 256/384/521介于两者之间视具体环境而定ed25519生成的公钥只有一行、长度很短辨认起来也方便。需要留意的一点是如果你的系统自带 ssh 工具版本特别老可能会报unknown key type ed25519这种情况下退回用 rsa 4096 就行功能上没有任何区别。3.3 别把私钥当公钥复制出去这一步是新手翻车的高发区必须说清楚。生成完之后.ssh目录下会多出两个文件ls -l ~/.ssh/ # id_ed25519 - 私钥没有后缀绝对不能外发 # id_ed25519.pub - 公钥要贴到 Gitee 上的就是它 # config - 配置文件如果自己建过 # known_hosts - 记录连接过的主机指纹id_ed25519.pub打开之后只有一行以ssh-ed25519开头中间是一长串字符末尾是你刚才-C写的备注。要上传的就是这一整行的内容。很多人在这里会因为复制方式出问题。比如用图形化的文本编辑器打开.pub文件全选复制的时候不小心多带了前后空白或者中间断行又比如在 Windows 上误以为需要用某种特殊方式转换格式。最稳的做法是在终端里把内容直接打出来再复制# macOS / Linux / Git Bash cat ~/.ssh/id_ed25519.pub # Windows PowerShell / CMD type $env:USERPROFILE\.ssh\id_ed25519.pub还有一种更常见的乌龙把没有.pub后缀的私钥内容贴到了平台上。私钥开头是-----BEGIN OPENSSH PRIVATE KEY-----一眼就能认出来贴上去平台一定会提示格式错误。记住一句话带 .pub 的往外给不带 .pub 的老实待在本机。4. 把公钥登记到 Gitee再确认真的通了4.1 添加公钥的位置和命名习惯登录 Gitee 之后进入个人设置找到 SSH 公钥相关的页面点添加公钥。会看到两个必填项标题和公钥内容。标题随你写但我强烈建议养成一个好习惯写清楚设备和使用场景比如「家里台式机-2024」「公司笔记本-个人项目」。原因很现实——一年之后你可能会挂上四五把公钥某天想清理其中一把已经不用了的如果标题全是「我的公钥」「public key」这种你根本分不清哪把是哪把最后只能全删重来那才是最麻烦的。公钥内容就是刚才cat出来的那一整行注意只贴一行不要贴多行也不要手动加换行。4.2 用什么命令验证才算成功配完之后必须验证不要直接就去 push。验证命令是ssh -T gitgitee.com注意这里的-T是大写 T作用是禁用伪终端分配让这条命令只做认证测试然后立刻返回不会真的给你一个交互式会话。如果一切正常你会看到类似这样的输出Hi 你的用户名! Youve successfully authenticated, but GITEE.COM does not provide shell access.看到这行字就是成功了。这里有个很常见的误判——很多人看到does not provide shell access就以为哪出错了。这句话的意思只是「Gitee 不提供 SSH 命令行外壳访问」本来就是正常的因为它只是一个代码托管平台不需要给你一个登录进去敲命令的 shell。前半句Hi 你的用户名才是关键它证明服务端已经通过公钥认出了你是谁。如果输出依旧是Permission denied (publickey)那就往下走排查流程别在这里反复试。4.3 首次连接的指纹确认如果这是你的机器第一次连接 Giteessh -T之前很可能会先弹出一段提示The authenticity of host gitee.com (x.x.x.x) cant be established. ED25519 key fingerprint is SHA256:xxxxx. Are you sure you want to continue connecting (yes/no/[fingerprint])?这是在问你是否信任对方主机的身份。必须手动输入完整的yes直接回车或者输个y都不算会被当成拒绝。确认之后这条主机的指纹会被写进~/.ssh/known_hosts以后不再提示。这一步背后的逻辑是防止中间人冒充第一连接时客户端没有对方的任何记录只能让你人工确认一次。所以如果哪天突然又弹出一模一样的提示说主机的指纹变了那就要留个心眼别条件反射地打yes先去看看是不是自己网络环境变了。4.4 别忘了把 remote 改过来公钥验证通过了但如果你之前克隆时用的是 HTTPS 地址现在还得把远程地址切换成 SSH 形式否则 push 的时候依然走老通道。# 如果已经有 origin git remote set-url origin gitgitee.com:yourname/yourrepo.git # 如果还没加过 git remote add origin gitgitee.com:yourname/yourrepo.git # 确认 git remote -v这一步在「密钥配好了但还是推不上去」的案例里占比相当高属于纯低级失误但极其常见值得单独强调一次。5. 密钥配完了还是报错一条完整的排查链路5.1 用-vT看握手卡在哪一步到了这一节说明前面的标准流程你都走了但ssh -T gitgitee.com还是给你甩Permission denied (publickey)。这时候不要再猜直接开调试模式ssh -vT gitgitee.com-v会把 SSH 握手的每一步打印出来信息量很大。想看得更细可以用-vvv但对这条报错来说-v已经足够。输出里重点盯这几类行debug1: Offering public key: /c/Users/you/.ssh/id_ed25519 ED25519 SHA256:xxxx debug1: Authentications that can continue: publickey debug1: Next authentication method: publickey第一种情况Offering public key后面出现的路径根本不是你刚配好的那把。它可能是id_rsa可能是某个远古时期留下的密钥也可能是某个第三方工具塞进去的。这说明客户端压根没拿正确的钥匙去开门问题出在本地选钥匙的环节往下看第 5.3 和 5.4 节。第二种情况Offering public key后面出现的就是那把正确的钥匙服务端也收到了但最后仍然Permission denied。这说明钥匙递对了但名册上没有它。问题几乎可以确定在 Gitee 侧的公钥登记上往下看第 5.2 节。这两种情况区分清楚排查范围瞬间就缩小了一半。这就是为什么我一直劝人先看日志再动手比盲目重装靠谱得多。5.2 本地公钥和平台上的内容对不上如果日志显示钥匙递对了但服务端不认第一步是把本地公钥的实际内容和 Gitee 上登记的内容做逐字符比对。cat ~/.ssh/id_ed25519.pub然后打开 Gitee 的公钥列表两边对照。常见的偏差有这么几种一是复制的时候内容被编辑器自动折行或者截断中间少了一段二是你贴上去的是旧密钥本地生成时不小心覆盖了原来的文件但平台上还挂着旧的那把三是同一台机器上存在多个同名文件你cat的和实际被使用的是两个不同的东西。最干脆的做法是把 Gitee 上那把删掉重新cat一次整行复制重新粘贴保存再验证一遍。这种「推倒重来」比在两边找差异要快得多。另外提一句公钥末尾的注释部分就是你-C写的那串即使和平台上的不完全一致也不影响验签SSH 校验的是密钥本身那一大段注释只是给人看的。所以不要因为备注不一样就以为配错了。5.3 密钥托管程序里的缓存在 macOS 和一些配置了密钥托管程序的 Linux 上系统会把私钥加载到内存里之后 ssh 优先用内存中的这些钥匙而不去读磁盘。这时候你明明在磁盘上生成了新钥匙ssh 却还在用老的了。# 查看当前加载了哪些密钥 ssh-add -l # 清空重新加载或者单独加载指定的一把 ssh-add -D ssh-add ~/.ssh/id_ed25519Windows 上对应的机制是系统服务「OpenSSH Authentication Agent」可以在服务列表里看它是否在运行。如果它在运行且里面缓存过旧密钥同样会出现「磁盘上有新的、实际用的是旧的」这种诡异现象。提示ssh-add -D是清空所有已加载密钥敲之前确认一下你有没有正在依赖某把钥匙的会话虽然一般情况下清了也不影响已经建立的连接。5.4 权限位不对密钥会被静默忽略这是 macOS / Linux 上非常经典的一个坑。SSH 客户端对私钥文件的权限要求很严格如果权限过于开放比如 644 甚至 777它会直接跳过这个文件连读都不读表现得就像文件不存在一样。这种情况下你在日志里会看到类似Permissions 0644 for xxx are too open或者干脆no such identity的提示。修正方式chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub chmod 600 ~/.ssh/config记住这个对应关系目录 700私钥 600公钥和 known_hosts 可以 644config 建议 600。用ls -l ~/.ssh看一眼就能确认现状。为什么权限这么要紧因为私钥本质上就是一把无需任何附加信息的万能钥匙谁读到都能冒充你本地文件系统层面必须先替你挡一道。SSH 宁可错杀也不放过权限不合规就当作没这把钥匙。5.5 Windows 上的双份 SSH 更容易出事Windows 是这条报错的重灾区核心原因是一台机器上往往存在两套甚至更多的 SSH 客户端一套是系统自带的装在C:\Windows\System32\OpenSSH\下另一套是 Git for Windows 附带的在C:\Program Files\Git\usr\bin\ssh.exe。还有一个常见的来源是各种编辑器或 IDE 自带的终端环境。这两套客户端在默认情况下会去看同一个C:\Users\你的用户名\.ssh\目录所以密钥本身是通用的但它们的配置文件、known_hosts、以及密钥托管程序的行为可能完全不同。于是就会出现一种魔幻场景在 Git Bash 里ssh -T是通的在 PowerShell 里同样的命令就报Permission denied。排查方法就是确认自己当前到底在用哪一个# PowerShell Get-Command ssh # CMD where ssh输出里的路径会告诉你答案。要统一的话要么把两边的配置都写好要么在环境变量GIT_SSH_COMMAND里显式指定使用哪一个可执行文件。我在 Windows 上的做法是固定用一个终端环境做 git 操作把配置只维护一份避免这种双份环境互相对不上。此外还有一个残留问题如果你之前用 HTTPS 推过代码Windows 的凭据管理器里可能存着那时的账号信息后来切到 SSH 之后它虽然不再起作用但偶尔会在某些工具里造成干扰清理一下更干净。5.6 排查顺序参考表把上面这些串起来可以整理成一张按现象定位的速查表遇到问题时从上往下走。现象大概率原因验证命令日志里递出的密钥路径不对存在多把密钥默认选了别的ssh -vT gitgitee.com密钥路径正确但仍被拒平台公钥内容不一致或已删除对比cat ~/.ssh/xxx.pub提示密钥权限过开放私钥权限位不对ls -l ~/.ssh、chmod 600磁盘上有新密钥但没被使用密钥托管程序缓存了旧密钥ssh-add -l、ssh-add -D同一命令在不同终端结果不同Windows 存在多套 SSH 客户端where ssh加了 config 之后反而失败config 里的 Host 段写法有误ssh -F none -T gitgitee.com最后一行这个技巧特别好用-F none表示忽略配置文件做一次干净连接。如果加上-F none之后成功了那就百分之百说明问题出在~/.ssh/config里接下来只管去修配置其他什么都别动。6. 一份 config 让多平台多账号互不打架6.1 为什么一旦钥匙变多就必然出事默认状态下ssh 客户端在~/.ssh目录里按固定顺序去猜文件名比如id_rsa、id_ecdsa、id_ed25519。它对「哪把钥匙对应哪个平台」一无所知只会一个接一个地把手上的钥匙试过去。一把钥匙的时候这没问题一旦你有了工作账号和个人账号两把钥匙或者同时在用 Gitee 和别的托管平台默认行为就会开始捣乱它可能先拿工作那把去试个人仓库试失败了还没结束继续往下试试到第五六把的时候某些服务器会因为失败次数过多直接断开报出另一条完全不同的错误。所以钥匙一多就必须靠 config 手动指定。6.2 一份可直接抄的配置在~/.ssh/config里写下面这样的内容文件不存在就新建一个Host gitee.com HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_gitee IdentitiesOnly yes逐行拆解一下。Host是你自己给这段配置起的标识名也是你在命令行里要写的那个名字HostName才是真正要连的服务器地址两者可以不同这个差别在下一节讲。User git是因为前面说过SSH 访问 Gitee 时用户名固定是git。IdentityFile指定这道门要用哪把钥匙路径最好写绝对路径虽然~一般也能识别但遇到某些环境下不展开的情况会很头疼。最关键的是最后一行IdentitiesOnly yes。它的意思是只使用 IdentityFile 指定的这把钥匙不要把你手上其他的钥匙也一股脑递上去。少了这一行前面说的「钥匙串号」问题会照旧发生只不过顺序上稍微改善一点而已。我见过太多人 config 写了但忘了这一行然后疑惑为什么还是不稳定——这行才是整套配置的点睛之笔。6.3 同平台两个账号的正确写法有一种更复杂的情况你在一台机器上要用两个 Gitee 账号一个工作一个个人。这时候Host名就不能都写gitee.com了得用别名区分Host gitee-work HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes Host gitee-personal HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes注意两段的HostName都是gitee.com只有Host标识和IdentityFile不同。配置好之后仓库的远程地址也要相应改掉把主机名部分换成别名git remote set-url origin gitgitee-work:company/yourrepo.git验证时同样用别名ssh -T gitgitee-work ssh -T gitee-personal两个命令返回的Hi后面应该是不同的用户名这就说明两把钥匙各归各位了。提示一个 Gitee 账号下同一把公钥只能绑定一次两个账号必须用两把不同的公钥。想复制同一份公钥到两个账号下是行不通的服务端会直接拒绝或者后绑定的那个无法生效。6.4 改完配置怎么确认真的生效配置写完之后别急着 push先做两件事。第一件是确认语法没写错。最省事的办法还是拿-vT跑一遍看日志里的Offering public key后面出现的路径是不是正好是你IdentityFile里指定的那一个。是就对了不是说明Host那段根本没被匹配上多半是主机名写得和实际使用的不一致。第二件是注意Host匹配的规则。你在命令行里敲的是gitgitee-work:xxx/yyy.gitssh 会拿gitee-work这个字符串去匹配 config 里的Host行而不是匹配HostName。所以只要命令行里的主机名和Host对不上你这整段配置就形同虚设ssh 会退回默认行为去瞎试钥匙。这个细节几乎是所有人第一次写多账号配置时都会踩的坑。另外如果你想让所有到 Gitee 的连接都走某一段公共配置比如统一的密钥可以在文件开头写一个Host *段落作为默认值下面再用具体的Host覆盖。不过对刚上手的人来说我建议先别用通配把每一段写死、写清楚等真用顺了再考虑抽象。折腾到这一步其实这套流程已经覆盖了绝大多数场景。我个人这些年换机器、重装系统的固定动作就四步生成密钥、把.pub贴到平台、ssh -T验一次、顺手把~/.ssh/config建好。整个过程五分钟以内比事后花半小时对着Permission denied发愁要划算得多。唯一想再唠叨一句的是密钥一旦生成就把.ssh目录当成本机最私密的角落对待别随手备份到云盘或者聊天记录里传给自己那把没有.pub后缀的文件泄露一次就等于把门钥匙交给了所有人。