资讯动态

Git子模块拉取报错publickey:SSH认证与sync排查指南

发布时间:2026/10/17 7:58:16 来源:尧图企业网站定制
如果你在某次 clone 完主仓库之后跟着执行git submodule update --init --recursive屏幕上却蹦出一行Permission denied (publickey)先别急着改文件权限也别反反复复ssh-add。这个报错我在实际工作中前前后后遇到过七八次其中有一次因为先入为主地认为是密钥没配好折腾了大半天最后才发现问题根本不是密钥而是子模块的 URL 配置改了之后漏了一步同步操作。这篇内容不打算做成一本正经的官方文档我想把 Git 拉取子模块时的 publickey 报错整个拆开它最常见的触发原因是什么、排查顺序应该怎么排、SSH 认证链路里到底哪一环会断、多密钥场景下又该怎么配最后再给你一套可以直接抄的检查清单。如果你正在被这个报错卡住或者打算把排查方法沉淀到团队文档里这篇应该能帮你省下不少时间。1. 报错现场还原主仓库能拉子模块却被拒这个报错最迷惑人的地方在于很多时候主仓库明明是正常 clone 的偏偏拉子模块的时候就失败。这就产生了一个错觉——我本地网络没问题、Git 也能连远端那问题肯定出在密钥文件上。实际上主仓库能拉和子模块能不能拉是两件独立的事不能混在一起判断。1.1 两种典型的一半成功一半失败我遇到的现场基本可以归成两类。第一类是主仓库用 HTTPS 地址子模块却配了 SSH 地址。团队里很常见主仓库地址大家从网页上拷贝默认就是 HTTPS而某个子模块仓库因为有写权限需求维护者习惯性地在.gitmodules里填了git...这种 SSH 格式。这时候你本地如果没有登记过 SSH 公钥就会看到 publickey 报错而主仓库因为走的是 HTTPS 凭据或用户名密码完全不受影响。第二类是主仓库和子模块都是 SSH但主仓库能 clone、子模块仍然报 publickey。这种情况通常意味着 SSH 认证链路在子模块所在的远端这一层断了。比如子模块放在另一个独立域名的代码仓库上而你的公钥只登记在主仓库所在的平台又或者 agent 里加载的私钥和该子模块要求的不是同一把。总之子模块相当于一次全新的远程仓库访问主仓库的认证通过并不会自动传导给子模块。很多刚接触 Git 子模块的人会在这里被带偏看到主仓库能拉就认定自己的 SSH 环境没问题于是反复检查本地~/.ssh目录越查越糊涂。正确的处理心态是把主仓库和子模块当成两个独立的认证对象分别判断。1.2 publickey这行字到底在说什么Permission denied (publickey)这行信息本身是有价值的。它说明 SSH 的 TCP 连接已经建立、SSH 协议已经跑通但服务器在检查客户端身份时没有找到任何它认识的公钥。可以把 SSH 认证理解成进一栋需要门禁的楼客户端先报上名字再出示一种钥匙私钥对应的公钥服务器在自己的授权名单里查这枚公钥查到就放行查不到就会拒绝并提示Permission denied (publickey)。注意这个阶段完全不涉及主机指纹校验那是更早一步的确认这栋楼是不是你要找的那栋楼。Git 里常见的几个 SSH 相关错误要分清楚不然排查方向会跑偏报错内容实际含义排查方向Permission denied (publickey)身份认证失败服务器不认你的公钥密钥文件、agent、远端授权Host key verification failed服务器指纹不在 known_hosts 或指纹变化检查 known_hostsConnection refused / timeout网络层都没通网络连通性、端口、服务状态如果看到Permission denied (publickey)但网络是通的说明问题层级停留在认证不是网络也不是 Git 配置问题。明确了这一点后面所有的排查才有方向。2. 先看 .gitmodules子模块地址的协议决定认证方式很多人一拿到 publickey 报错就直接钻到 SSH 配置里去翻其实第一步应该看的是子模块本身使用什么协议。子模块的取数地址写在.gitmodules文件里这个文件决定子模块的远端 URL 形态而 URL 的协议形态直接决定 Git 会走哪一套认证流程。2.1 三条命令看清子模块的远程地址建议按下面的顺序先把现场信息捞出来cat .gitmodules这一步会直接显示当前仓库声明的所有子模块。重点看每个子模块的url字段判断它是 HTTPS 地址、SSH 地址还是相对路径。git config --list | grep -i submodule这一步看的是子模块在本地 Git 配置中的实际值后面会专门讲它和.gitmodules的关系这里先习惯性地看一眼。git submodule status这一步能看到子模块当前的检出状态。如果子模块完全没拉下来它前面通常会有一个-前缀。.gitmodules里常见的 URL 无非几种以https://开头的是 HTTPS以git开头或者ssh://开头的是 SSH还有一种以./或../开头的相对路径Git 会根据父仓库的 URL 自行推导。2.2 URL 的协议形态为什么这么关键如果子模块 URL 是 HTTPSGit 会走 HTTPS 认证流程通常使用凭据管理器、用户名密码或 token。理论上来说HTTPS 流程不会返回Permission denied (publickey)如果返回的是其他认证失败提示那就是 HTTPS 凭据问题。反过来如果 URL 是git或ssh://开头就必须走 SSH 公钥认证此时出现Permission denied (publickey)才符合预期。这也是我在排查时最先看.gitmodules的原因先判断报错方向和协议是不是匹配的。如果 URL 明明是 HTTPS 却报 publickey那概率极小多半是前面有一层 SSH 包装或者 url 重写规则在起作用而 URL 如果明确是 SSH那就安心顺着 SSH 链路往下查。这里也顺手分享一个经验团队内部子模块的协议最好统一不要主仓库一个协议、子模块另一个协议。协议混用的仓库基本都会在某个新同事的机器上触发一次这样的报错纯粹是给自己人添堵。2.3 相对路径子模块更容易引向错误方向有一种更隐蔽的情况.gitmodules里的 URL 是类似../lib.git这样的相对路径。Git 解析这种 URL 时会基于父仓库的 URL 来做推导。也就是说如果你 clone 父仓库时用的是 A 地址实际访问子模块时可能被重写成一个完全不同的 B 地址如果你在本地配置了url.insteadOf之类的改写规则推导结果也会跟着变。相对路径不是不能用但一旦出现 publickey 报错很多人都懵了明明自己在.gitmodules里看到的地址是../lib.git根本不涉及任何主机名怎么还会报主机认证失败这时候不要盯着相对路径本身而是要看父仓库 clone 用的真实 URL 是什么因为子模块实际访问的就是父仓库 URL 推导出来的那个远端。3. 密钥链路的四层体检文件、agent、远端一个都别跳排到这一节才算真正进入 SSH 认证链路本身。链路大致是私钥文件存在且权限正常 → ssh-agent 正确加载该私钥 → SSH 客户端到远端进行认证 → 远端授权名单里存在对应公钥。任何一层断了都有可能出现 publickey 报错。下面我按顺序讲不建议跳步。3.1 第一层密钥文件是否存在、权限是否正常先在~/.ssh目录下看实际文件ls -la ~/.ssh/常见私钥文件名是id_rsa或id_ed25519对应公钥文件是id_rsa.pub或id_ed25519.pub。如果这个目录里什么都没有说明你根本还没生成过密钥对那就直接生成一个ssh-keygen -t ed25519 -C your_emailexample.com一路回车即可生成完之后用下面两条命令确认权限目录~/.ssh最好是 700私钥文件必须是 600公钥可以是 644。chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519为什么要强调权限OpenSSH 对私钥文件权限非常敏感。如果私钥文件权限过宽比如 644SSH 客户端会直接拒绝使用这个私钥宁可继续尝试下一个。很多人在这一步卡住不是因为没有密钥而是因为密钥文件的权限太开放了。Windows 上还有一个容易忽略的点直接对C:\Users\你的用户名\.ssh执行 chmod 不完全等同于 Linux 权限模型。如果你无论如何设置权限仍然提示 publickey可以检查一下目录是否被 Windows 同步服务或杀毒软件占用必要时用管理员权限打开 PowerShell先icacls收紧权限再试。3.2 第二层ssh-agent 是否递出了正确的钥匙密钥文件存在只是前提SSH 客户端实际认证时需要从 ssh-agent 里取私钥。ssh-add -l这条命令会列出 agent 当前加载的所有私钥指纹。如果提示The agent has no identities说明 agent 里是空的需要手动加载eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519第一次执行ssh-add时如果私钥设置了 passphrase会提示输入密码。之后 agent 会一直保留这个私钥当前会话内不需要重复输入。还有一个非常实际的坑如果 agent 里同时加载了多把私钥OpenSSH 会逐个尝试直到服务器接受了其中一把。某些远端对认证失败次数有严格限制试到一定次数就直接断开返回类似too many authentication failures或直接表现为 publickey 失败。所以并不是钥匙越多越好有时候反而需要精简 agent 里的密钥数量或者用后面章节提到的IdentitiesOnly严格控制尝试范围。3.3 第三层用 ssh -T 与 ssh -v 做现场实测当本地文件、agent 都看起来没问题时用一个最小命令直接验证远端认证ssh -T gitexample.com-T的含义是不分配交互终端这是 Git 类场景最常用的验证方式。如果认证通过远端通常会返回一条欢迎信息比如告诉你当前认证用户是谁如果认证失败还是会回到Permission denied (publickey)。如果ssh -T也是同样的报错那就再加一个参数看细节ssh -vT gitexample.comverbose 输出会比较长不用从头读重点找几类关键日志看到Offering public key字样说明 SSH 客户端已经在尝试出示你本地的某把公钥看到Send pubkey后立刻跟着授权失败信息说明公钥被远端拒绝了看到日志里根本没有Offering public key这一行说明 SSH 客户端压根没打算拿你手头的那把钥匙去试。我的习惯是拿ssh -vT的输出作为分水岭日志里有Offering且被拒问题在远端授权日志里没有Offering问题在本地选钥匙的逻辑多半需要检查~/.ssh/config或 agent 加载情况。3.4 别把 host key 验证失败和 publickey 搞混排查过程中经常有人误伤known_hosts。Host key verification failed和Permission denied (publickey)完全是两个层面的问题。known_hosts记录的是远端服务器的身份指纹。当远端服务器重建、更换密钥或者全新主机首次连接时SSH 会提示 host key 校验失败。这时可以用ssh-keygen -R 主机名删除旧指纹后重新连接。但不要因为看到 publickey 报错就去清空known_hosts这两者没有关系。清空之后只是多冒出一条确认 host key 的提示并不会让你的公钥突然被远端接受。我在带团队时最常看到的场景是同事 publickey 报错自己把~/.ssh/known_hosts整个删掉了然后发现报错依旧这才回头认真查密钥文件。排查链路一旦跳步时间成本翻倍。4. 多密钥场景如何让不同子模块认准不同的钥匙如果你只有一把私钥而且已经登记到远端平台那 publickey 报错大概率不用走到这一节。但现实是很多开发者一台机器上既有个人项目又有团队内部项目还可能对接多个不同的 Git 托管平台每套环境各发一把密钥。这时候光有密钥还不够麻烦的是 SSH 客户端不知道该用哪一把去试哪台主机。4.1 为什么钥匙都在 agent 里反而坏事有些人习惯把所有私钥一股脑全部ssh-add进 agent觉得一定能覆盖所有情况。实际效果恰恰相反agent 里钥匙越多SSH 客户端越容易乱序尝试。当 SSH 客户端尝试了一把某托管平台根本不认识的公钥后对方会拒绝接着尝试下一把如果钥匙数量很多可能还没轮到正确的那把对方已经因为尝试次数过多断开了。另一个常见现象是你的私钥文件名不是默认的id_rsa或id_ed25519而是类似company_key、personal_ed25519这种自定义名字。Git 调用 SSH 时如果没有显式指定使用哪个私钥文件OpenSSH 默认只找那几个固定的默认文件名。这就造成了钥匙明明在磁盘上、而且权限正确但 Git 就是用不上它的尴尬。解决这个问题的思路不是继续往 agent 里加钥匙而是让 SSH 客户端能分辨访问哪台主机时应该出示哪把钥匙。4.2 用 ~/.ssh/config 给每个 Host 指定专属密钥~/.ssh/config是 OpenSSH 的客户端配置文件。它的作用很像一个路由表能根据你连接的 HostName 或别名决定使用哪个私钥文件、使用哪些参数。下面是一个常见的多密钥配置示例Host work HostName git.example.com User git IdentityFile ~/.ssh/company_key IdentitiesOnly yes Host personal HostName git.personal.example.com User git IdentityFile ~/.ssh/personal_ed25519 IdentitiesOnly yes其中Host后面的别名会出现在 Git URL 中HostName是真实的远程主机名IdentityFile显式指定私钥文件IdentitiesOnly yes强制客户端只使用这里指定的密钥不要理会 agent 里的其他钥匙。配置完之后原来 URL 写的是gitgit.example.com:team/subproject.git现在可以在.gitmodules里改成gitwork:team/subproject.git。SSH 客户端一看到work这个别名就会找到配置里对应的私钥文件而不会拿 agent 里的一堆密钥去瞎试。我这里特别强调一下IdentitiesOnly yes。它是多密钥配置里的关键参数不加这一行即使你写了IdentityFileagent 里的其他密钥依然可能被拿去尝试问题依旧。加上之后认证尝试被严格限定在你指定的私钥上既快又干净。4.3 但别直接把本地别名提交进团队仓库用 Host 别名来区分密钥很方便但有一个必须注意的界限.gitmodules是随仓库提交的会影响所有协作者。如果你把gitwork:...这种本地别名写进.gitmodules别人没有对应的~/.ssh/config拉取时就会一头雾水。所以正确的做法是团队仓库里的子模块 URL 统一用完整的合法地址个人本地可以通过git config覆盖或者 url 改写来适配自己的多密钥环境。这一点我会在下一章专门展开因为它和另一个常见坑——改完配置仍然报错——高度相关。5. 改完 .gitmodules 还报错十有八九忘了 git submodule sync这一节是我最想重点讲的经验因为很多人已经在前面查完了密钥、确认了权限甚至把.gitmodules里的 URL 改成了正确地址执行git submodule update却依然报 publickey。问题往往出在一个极少会想到的命令上git submodule sync。5.1 子模块 URL 其实存在两份很多人习惯性地认为子模块地址只有.gitmodules一份其实还藏了另一份在本地 Git 配置里。一个子模块被初始化时Git 会做两步操作先读取.gitmodules然后把 URL 写入当前仓库的.git/config生成形如submodule.名称.url的配置项。之后真正执行 fetch 时Git 默认使用的其实是.git/config里保存的 URL 地址。这带来的直接影响就是如果你只改了.gitmodules没有同步到.git/config那 Git 实际去 fetch 子模块时用的还是旧地址。旧地址认不了你的公钥自然继续报Permission denied (publickey)。我用生活点的比喻.gitmodules是子模块的登记表.git/config是本次执行用的调度单。改了登记表却没重新下发调度单干活的时候拿的还是旧单子。5.2 什么时候必须跑 sync以及命令怎么带.gitmodules里 URL 被修改过、整个仓库从旧地址迁移到新地址、通过git config手动改过子模块地址这三种情况都需要重新同步。基础命令是git submodule sync如果子模块里面还有嵌套子模块建议直接带上--recursivegit submodule sync --recursive执行完 sync 后再拉取子模块git submodule update --init --recursive同步之后可以用下面的命令确认.git/config里的地址已经更新git config --list | grep ^submodule看到输出的 URL 和.gitmodules一致才说明这次修改真正生效了。我印象很深的一次事故是团队仓库从旧域名迁移到新域名运维同学在.gitmodules里批量替换了所有 URL大家拉取时仍然一个接一个报错。最后发现每个人本地git config里的子模块 URL 都还写着旧域名。挨个执行git submodule sync之后问题立刻消失。这个命令值得刻在脑门上。5.3 只想本地改 URL 不污染团队的几种姿势如果你为了适配自己的多密钥场景或者临时使用一个镜像源需要修改子模块 URL但又不希望提交到仓库里影响队友有两种干净的办法。第一种是直接修改本地.git/configgit config submodule.名称.url gitexample.com:team/subproject.git这种改动只存在于当前仓库的本地配置.gitmodules完全不受影响队友也不会被波及。第二种是利用url.insteadOf做全局的地址改写git config url.https://example.com/new/.insteadOf gitexample.com:old/这个配置对符合前缀的所有 URL 生效适合大批量替换场景但要注意它的影响范围比第一种大。如果你只想精确控制某一个子模块我建议优先用第一种方式。6. 一张可以直接照做的排查清单与团队建议最后把这几年和这个报错交手的心得收敛成一套可复用的动作。遇到 publickey 报错按顺序过一遍能大幅缩短定位时间。6.1 按顺序执行的自检清单检查项对应命令/动作通过条件子模块协议cat .gitmodules确认 URL 是否走 SSHHTTPS 报 publickey 本身就很异常远端连通ssh -T gitexample.com返回欢迎信息视为通过继续报 publickey 则继续密钥文件ls -la ~/.ssh/存在对应私钥权限不松散agent 状态ssh-add -l包含目标私钥的指纹详细日志ssh -vT gitexample.com能看到Offering public key且被接受子模块同步git submodule sync --recursive确认.git/config的 URL 与.gitmodules一致远端授权托管平台侧检查对应公钥已登记到平台或仓库授权名单这套清单的核心逻辑是先判断方向再检查本地环境最后才怀疑远端授权。别一开始就发工单问平台管理员大部分时候问题都在自己这儿。6.2 放进团队文档的命令与建议如果你的团队经常有新人加入建议在内部文档里直接写一段标准初始化流程git clone 仓库地址 cd 仓库目录 git submodule sync --recursive git submodule update --init --recursive新同事环境配置环节也可以建议他们完成这三件事生成 SSH 密钥、把公钥登记到平台的 SSH keys 设置页、执行ssh -T gitexample.com确认远端认证通过。这三步做完了大部分子模块 publickey 问题在首次接触项目前就被扼杀了。如果 CI 构建环境里也在拉取子模块记得 CI 环境也要配置单独的部署密钥并在构建脚本里加入 sync 和 update 命令防止线上构建因为同样的坑留白。6.3 我自己实践后的两点习惯我自己的习惯是这样的第一.gitmodules里永远不写本地 Host 别名只写完整的合法地址这个是底线第二拉取包含子模块的仓库前无条件先执行一遍git submodule sync --recursive再执行 update哪怕我确定没有改过任何配置多跑一步也不会有坏处。另外每次更换电脑或重新装机后我都会第一时间执行ssh-add并确认 agent 里加载的私钥指纹。这个微小的习惯帮我省掉了不少和报错信息对视的时间。如果你也经常被 Git 子模块的 publickey 报错绊住不妨先从.gitmodules看起再按链路一层层查下去很多时候答案其实就在离报错不远的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑