Git SSH 认证、多账号与代理
Git 使用 SSH 远端时,真正执行认证的是 SSH 客户端。Git 只把类似 git@code.example.com:team/service.git 的地址交给 ssh;ssh 再读取配置、选择私钥或 agent 中的身份、验证服务器主机密钥,必要时经过跳板机,最后向托管平台证明“客户端持有某个私钥”。
因此,SSH 认证有两条不能混淆的信任链。客户端公钥认证回答“谁在访问仓库”;known_hosts 回答“客户端连到的是否是预期服务器”。只把公钥上传平台而忽略主机指纹,或为了省事关闭主机校验,都只完成了一半安全设计。
先把“能连上”拆成四个判断
最典型的现场不是完全连不上,而是个人仓库正常、工作仓库返回 Permission denied (publickey);命令行正常、IDE 推送失败;办公室直连正常、回家经过跳板超时。继续删除密钥和重试只会增加变量,因为一次 Git SSH 访问至少经过四个判断:
Git 根据远端 URL 调用了哪一个 ssh,以及本地 Host 别名是否命中。SSH 从配置文件、私钥文件和 agent 中选择并提供了哪个客户端身份。客户端能否用 known_hosts 中的可信主机密钥确认服务器身份。
托管平台把公钥映射到哪个账号,以及该账号是否拥有目标仓库权限。
SSH 登录认证、SSH 格式的 Commit 签名和 DCO Signed-off-by 也是三种不同证据。登录认证证明客户端持有访问密钥,Commit 签名证明某个 Git 对象由签名密钥签署,DCO 则是提交消息中的贡献声明;即使平台允许登记同一把公钥,它们也不会因此变成同一件事。
准备一只没有业务数据的测试仓库,并确认当前账号有上传 SSH 公钥和创建临时分支的权限。企业组织若启用了 SSO 或密钥审批,还要让公钥完成组织授权。先记录本机真正使用的工具和远端:
git --version
ssh -V
git remote -vWindows 还应确认实际调用的是哪套 OpenSSH:
Get-Command ssh, ssh-keygen, ssh-add | Format-Table Name, SourceGit for Windows、Windows OpenSSH、WSL 和 IDE 可能各自携带或调用不同的 ssh。如果生成密钥、启动 agent 和执行 Git 的环境不是同一套,最常见结果是“明明加过密钥,Git 仍说 publickey 失败”。
第一次实验就遵循四条底线:
私钥只保存在受控终端或硬件密钥中,永不上传平台、仓库、聊天或工单。生成个人密钥时设置 passphrase,由 agent 缓存解锁状态;不要用空口令换便利。主机指纹必须通过平台官方页面、企业管理员或另一条可信渠道核对。
所有写入验证都在专用测试仓库和临时分支进行。
OpenSSH 的 ssh_config 手册定义了 IdentityFile、IdentitiesOnly、StrictHostKeyChecking、ProxyJump 与 ProxyCommand 的真实行为,ssh-keygen 手册和 ssh-add 手册分别给出主机记录、指纹和 agent 身份的操作入口。托管平台的 SSH 用户名、端口、主机指纹、SSO 授权和测试响应并不统一;接入 GitHub、GitLab、Gitee、Bitbucket 或自托管实例时,应从目标平台的 SSH 设置页取得这些值。下文使用的抽象域名和账号不能代替平台配置。
Windows
现代 Windows 可通过系统 OpenSSH Client 可选功能获得客户端;Git for Windows 也可携带 OpenSSH。优先沿用团队规定的一套,不要把两套 ssh.exe、两套配置目录和两个 agent 混起来。
先验证:
ssh -V
Get-Command ssh, ssh-keygen, ssh-add | Format-Table Name, Source
Get-Service ssh-agent若使用 Windows OpenSSH agent,可由管理员设置启动方式并启动服务,之后回到普通用户终端添加密钥:
# 管理员终端,仅做服务启用
Get-Service ssh-agent | Set-Service -StartupType Manual
Start-Service ssh-agent
# 普通用户终端
ssh-add $HOME/.ssh/id_ed25519_work
ssh-add -l如果团队使用 Git Bash 自己的临时 agent,就在同一个 Git Bash 会话中启动和使用,不要期待 Windows 服务自动读取 Git Bash 的 SSH_AUTH_SOCK。
macOS 与 Linux
多数系统已预装 OpenSSH 客户端。先执行 ssh -V,缺失时从系统官方包管理渠道安装。临时启动 agent 的通用方式为:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_work
ssh-add -l桌面系统可能已有会话级 agent;重复启动会产生多个 socket,让不同终端看到不同密钥。先检查 SSH_AUTH_SOCK 和 ssh-add -l,只有确实没有可用 agent 时才新建。
生成独立密钥
为工作身份使用能辨认用途、但不暴露真实邮箱或内部项目的文件名和注释:
ssh-keygen -t ed25519 -a 64 -C "work-code-access" -f ~/.ssh/id_ed25519_work交互时设置强 passphrase。生成后会得到私钥 id_ed25519_work 和公钥 id_ed25519_work.pub。只查看和上传 .pub:
ssh-keygen -lf ~/.ssh/id_ed25519_work.pubEd25519 是常见现代默认选择;受 FIPS、旧设备或企业策略约束时,算法由安全 owner 与平台兼容矩阵决定。支持 FIDO2 的团队可评估 ed25519-sk 等硬件密钥类型,把私钥操作绑定到硬件和触碰确认,但需验证开发机、远程环境和灾备密钥流程。
把公钥加入 agent 和平台
ssh-add ~/.ssh/id_ed25519_work
ssh-add -lssh-add -l 应显示与 ssh-keygen -lf 一致的指纹。然后在托管平台的 SSH keys 设置页上传 .pub 内容,并注明设备、用途和到期/复查信息。不要上传私钥,也不要用一个团队共享私钥代表多个自然人。
单账号配置
在用户 SSH 配置中声明平台主机:
Host code.example.com
HostName code.example.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
AddKeysToAgent yesIdentityFile 指定候选身份;IdentitiesOnly yes 表示即使 agent 里还有其他密钥,也只提供显式配置的身份。否则 agent 密钥过多时,服务器可能在试到正确密钥前就达到认证尝试上限。
OpenSSH 对大多数指令采用“取得的第一个值生效”,因此具体 Host 块放前面,Host * 默认项放最后。多个 IdentityFile 是少数会累加的配置,复制配置时尤其要注意。
同一平台多账号
为真实主机创建两个本地别名:
Host code-work
HostName code.example.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
Host code-personal
HostName code.example.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
Host *
ServerAliveInterval 30
ServerAliveCountMax 3项目远端必须使用别名,否则配置不会命中:
git remote set-url origin git@code-work:team/service.git这里的 code-work 只存在于本机 SSH 配置,网络连接仍去 code.example.com。提交作者 user.email 与 SSH 登录身份仍彼此独立;两者都应正确,但不能靠修改一个来修复另一个。
检查最终生效配置
ssh -G 只展开配置,不发起网络连接,是多账号排障的第一入口:
ssh -G code-work | grep -E '^(hostname|user|identityfile|identitiesonly|proxyjump) 'PowerShell:
ssh -G code-work | Select-String '^(hostname|user|identityfile|identitiesonly|proxyjump) '预期 hostname 指向真实平台,identityfile 只包含工作密钥,identitiesonly yes。若出现多个意外身份,检查更早命中的 Host 块、Include 文件和系统配置。
配置优先级可以在不联网的临时文件中做反向实验。把通配块故意放到具体主机之前:
Host *
User wrong-user
IdentityFile ~/.ssh/id_ed25519_default
Host code-work
HostName code.example.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes保存为 ssh-config-order-lab 后只展开配置:
ssh -G -F ./ssh-config-order-lab code-work | grep -E '^(hostname|user|identityfile|identitiesonly) '预期能看到 user wrong-user,并且两个 identityfile 同时存在:普通标量取首次获得的值,IdentityFile 则会累加。这个反例证明“后面的具体配置会覆盖前面默认值”并不是 OpenSSH 的通用规则。实验结束后删除临时文件,不要拿真实私钥路径或主机名制作可分享日志。
主机密钥与 known_hosts
第一次连接看到的指纹不是装饰提示。它证明服务器身份,需要与平台官方公布值或企业管理员提供值核对后再接受。ssh-keyscan 只能从网络收集主机公钥,不能独立证明它可信;如果在同一条可能被劫持的网络上扫描并直接信任,仍可能记录攻击者的密钥。
常用检查:
ssh-keygen -F code.example.com
ssh-keygen -lf ~/.ssh/known_hosts自动化环境可以预置经可信渠道分发的 known_hosts,并使用严格校验。StrictHostKeyChecking accept-new 会接受新主机但拒绝已知主机的变更,适合部分动态环境;高敏感环境更适合 yes 和集中维护。不要使用 no 或把 UserKnownHostsFile 指向空设备作为长期方案。
跳板机与代理
内网 Git 服务通常不应直接暴露公网。OpenSSH 的 ProxyJump 会先登录跳板机,再从跳板建立到目标的 TCP 转发:
Host code-bastion
HostName bastion.example.net
User developer
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
Host internal-code
HostName code.internal.example
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
ProxyJump code-bastion跳板机和目标机分别认证、分别验证主机密钥。目标身份不需要复制到跳板机。ProxyJump 的跳板主机配置不会自动继承目标主机块,跳板自己的用户、密钥和端口要独立声明。
若企业只提供 HTTP CONNECT 或 SOCKS 代理,可使用经过批准的 ProxyCommand 和本机代理工具。例如 OpenSSH 手册展示了通过 nc 连接 HTTP 代理的思路,但 nc 参数在不同实现上不一致,Windows 也未必存在同名工具。团队应提供按平台验证过的命令模板,不让开发者从网络复制任意代理命令。
ProxyJump 与 ProxyCommand 竞争,先取得的配置会阻止后者生效。一个 Host 不要同时堆两种方案期待自动回退。
验证顺序应从“配置解析”逐步推进到“仓库写入”。每一步只回答一个问题。
配置是否选对身份
ssh -G code-work | grep -E '^(hostname|user|identityfile|identitiesonly|proxyjump) '
ssh-add -l确认最终主机、用户、密钥和代理均正确,agent 中能看到对应指纹。
主机和账号认证是否成功
ssh -T code-work首次连接先核对主机指纹。托管平台通常不提供交互 shell,成功消息也可能伴随退出码 1;应以平台官方规定的响应文本和账号标识判断,不要只看 $?。若需观察选钥过程:
ssh -vvT code-work调试日志会暴露主机、账号、密钥路径和网络拓扑,分享前必须脱敏。
Git 是否能读取目标仓库
git remote get-url origin
git ls-remote --exit-code origin HEAD认证成功只证明平台认识这把 key;ls-remote 成功才证明该账号能读取这个仓库。
测试仓库是否可写
git switch -c ssh-auth-check
git commit --allow-empty -m "验证 SSH 写权限"
git push -u origin ssh-auth-check
git push origin --delete ssh-auth-check
git switch -
git branch -D ssh-auth-check预期临时分支创建并删除。保护分支拒绝直推属于治理规则,不应通过更换密钥或扩大权限绕过。
新克隆
git clone git@code-work:team/service.git
cd service
git remote -v
git ls-remote --exit-code origin HEAD已有仓库从 HTTPS 切换到 SSH
git remote set-url origin git@code-work:team/service.git
git remote get-url origin
git ls-remote --exit-code origin HEAD协议切换不改变提交历史、作者或签名。切换前保留旧 URL 记录,验证失败时可对称回退:
git remote set-url origin https://code.example.com/team/service.git仓库专用命令覆盖
极少数不能修改用户 SSH 配置的场景,可在仓库级指定命令:
git config --local core.sshCommand "ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes"
git config --show-origin --get core.sshCommand这会影响该仓库的所有 SSH 调用,且路径可移植性差。团队长期方案优先使用 Host 别名;core.sshCommand 适合作为明确记录的例外,而不是散落在每个仓库里的秘密开关。
# 看 OpenSSH 版本与实际远端
ssh -V
git remote get-url origin
# 展开最终配置,不联网
ssh -G code-work
# 查看 agent 中密钥指纹
ssh-add -l
# 添加、删除单个身份;删除全部身份需谨慎
ssh-add ~/.ssh/id_ed25519_work
ssh-add -d ~/.ssh/id_ed25519_work
ssh-add -D
# 查看公钥或私钥对应的公钥指纹
ssh-keygen -lf ~/.ssh/id_ed25519_work.pub
# 查询和移除旧主机密钥
ssh-keygen -F code.example.com
ssh-keygen -R code.example.com
# 测试账号,再测试仓库
ssh -T code-work
git ls-remote --exit-code origin HEADssh-add -D 会移除当前 agent 的所有身份,可能中断其他会话;轮换单个账号应优先 -d。ssh-keygen -R 只删除本地记录,不代表新主机密钥可信,删除后仍需通过可信渠道核对新指纹。
Permission denied (publickey)
ssh -T 或 Git 操作被拒绝,没有密码回退。
先用 ssh -G 查看最终 hostname、user、identityfile 和 identitiesonly,再用 ssh-add -l 比对指纹,最后用 ssh -vvT 查看客户端实际提供了哪把 key。
公钥未上传或未完成组织 SSO 授权;agent 没有对应密钥;别名未命中;平台要求的 SSH 用户名错误;私钥权限或格式不可读。
上传正确 .pub,完成组织授权,加载对应私钥,纠正 Host 和远端 URL。不要把私钥重新复制到多个目录试运气。
ssh -T 返回预期账号,再用 git ls-remote 验证仓库可见性。
登录成了另一个账号
SSH 测试成功,但欢迎消息显示个人账号;目标工作仓库返回无权限。
检查远端是否使用 code-work 别名,ssh -G code-work 是否只有工作 IdentityFile,ssh-add -l 是否加载了大量身份。
远端仍使用真实域名;缺少 IdentitiesOnly yes;更早的通配 Host 块已写入另一个身份;同一默认密钥被两个流程误用。
使用两个明确别名和独立密钥,把具体块放在 Host * 前,限制只提供目标身份。
分别执行 ssh -T code-work、ssh -T code-personal,两者应返回各自账号;工作仓库只用工作别名。
Too many authentication failures
服务端在正确密钥被尝试前断开。
ssh-add -l 查看 agent 密钥数量,ssh -vvT 查看 offered public key 顺序。
agent 中有大量密钥,客户端默认逐一提供;配置里多个 IdentityFile 累加;IdentitiesOnly 未开启。
为目标主机设置唯一 IdentityFile 和 IdentitiesOnly yes,必要时从 agent 删除无关密钥,而不是提高服务端失败次数上限。
ssh -vvT 应只提供预期身份,普通 ssh -T 成功。
REMOTE HOST IDENTIFICATION HAS CHANGED
SSH 拒绝连接并指出主机密钥变化。
暂停连接,通过平台状态页、官方指纹页或企业管理员确认是否有合法轮换;核对 DNS、VPN、代理和目标主机是否变化。
合法主机密钥轮换、域名指向新服务器、跳板配置变化,也可能是 DNS 劫持或中间人攻击。
只有在独立可信渠道确认新指纹后,才用 ssh-keygen -R host 删除旧记录并接受/预置新值。不要直接删整份 known_hosts。
再次连接时核对新指纹,随后执行 ssh -T 和 git ls-remote。
直连成功,经过跳板机失败
在内网可访问,外网通过 ProxyJump 超时或认证失败。
分别测试 ssh -T code-bastion 和 ssh -vvT internal-code;用 ssh -G internal-code 检查 proxyjump;区分跳板认证、跳板到目标网络、目标认证三段。
跳板机独立配置未命中;跳板没有到目标端口的路由;目标 DNS 只在内网可解析;把目标密钥错误地放到跳板;ProxyCommand 抢先覆盖 ProxyJump。
为跳板单独配置用户和密钥,由网络 owner 开通最小目标端口;保持目标私钥在本机;清理冲突的代理指令。
先验证跳板,再验证目标 SSH 身份,最后运行 Git 读取和临时分支推送。
Windows、Git Bash、WSL 之间 agent 不一致
一个终端 ssh -T 成功,IDE 或另一个终端失败。
每个环境分别运行 Get-Command ssh/which ssh、ssh -V、查看 SSH_AUTH_SOCK 和 ssh-add -l。
使用了不同 ssh、配置目录和 agent;IDE 固定调用另一套客户端;WSL 不会自然共享 Windows agent。
团队为每种开发入口明确客户端和 agent;IDE 指向同一可执行文件,或按官方支持方案桥接 agent。不要通过复制私钥到所有环境解决。
终端、IDE 和实际 Git 命令都显示同一预期身份与远端。
SSH 的敏感对象不只有私钥:
私钥:证明客户端身份,必须加口令、限制文件权限、禁止进入备份外泄范围。agent socket:能代表已加载身份执行签名,不能随意转发给不可信主机。公钥登记:决定某个身份映射到哪个平台账号或仓库 deploy key,需要 owner 和用途标签。
主机密钥:证明服务器身份,必须通过可信渠道分发和轮换。跳板权限:决定谁能进入内网路径,应与代码仓库写权限分开授予和审计。
个人开发者使用个人 key;只读部署或自动化可使用平台支持的 deploy key、机器用户、SSH 证书或短期身份。不要把个人 key 放到 CI runner,也不要让多名开发者共用一把“团队 key”。共享私钥会让审计只能看到密钥,无法追溯自然人。
agent forwarding 默认关闭。即使远端无法导出私钥,它仍可能在会话期间请求 agent 代表你认证;GitHub 对 agent forwarding 的风险说明也强调了这一边界。确有跨主机 Git 操作需求时,只对明确可信的 Host 开启,agent 中只加载必要身份,并考虑 ssh-add -c、-t 或目的地约束等当前客户端支持的限制。
密钥一人一设备一用途:命名中包含用途或设备代号,不包含真实内部项目;平台登记项带 owner、设备、创建时间和复查日期。提供无秘密配置模板:模板包含 Host 别名、真实平台域名、User、IdentitiesOnly 和代理入口,不包含私钥、公钥正文、真实内网地址或个人用户名。集中发布主机指纹:由平台 owner 通过受控渠道维护当前指纹、轮换窗口和事件公告,开发者不依赖即时 ssh-keyscan 建立信任。
隔离账号与机器身份:个人 key、deploy key、机器人 key 和 SSH CA 证书有不同 owner、权限和撤销流程。进行轮换演练:先登记新公钥并验证,撤销旧公钥,再从 agent 和磁盘清理旧私钥;确认没有仓库、IDE 或自动化仍依赖旧 key。离职与丢机处置:先在平台/CA 撤销公钥或证书,再冻结跳板权限和设备会话,随后审计异常访问。删除丢失设备上的文件不是可依赖动作。
保留协议回退:SSH 通道因网络策略不可用时,可切换到经批准的 HTTPS/GCM;回退应验证身份,不允许在 URL 中临时塞 token。
团队可以定期收集“公钥指纹、owner、平台登记 ID、用途、到期日”,但不收集私钥。平台若支持 SSH CA 和短期证书,大规模组织可用集中签发与撤销替代永久公钥清单;代价是 CA 高可用、签发审计和客户端兼容性必须有人负责。
多账号正确性依赖 URL 与配置共同命中
只写 Host code-work 不够,仓库远端也必须使用这个别名。反过来,远端用了别名但 Host * 更早设置了身份,也可能选错 key。排障时同时保存 git remote get-url origin 和 ssh -G alias,比只测试一次 ssh -T 更可靠。
主机密钥轮换需要供应链式发布
大规模平台更换主机密钥时,如果没有提前发布新指纹、双轨窗口和回退说明,开发者会形成“报错就删 known_hosts”的危险习惯。平台 owner 应让变更可验证;客户端 owner 应提供只删除目标条目的命令,而不是清空整库。
agent 便利性扩大了会话攻击面
agent 让私钥不必反复解锁,但任何能访问 socket 的进程都可能请求签名。共享跳板、远程开发容器和桌面插件较多时,应缩短 key 生命周期、按用途拆 agent、对高权限 key 使用硬件确认,并避免全局 ForwardAgent yes。
跳板机不是透明网络管道
ProxyJump 引入独立的账号、主机密钥、DNS、路由、端口策略和审计责任。排障必须分成“本机到跳板、跳板到目标、目标仓库授权”三段;容量上还要考虑大量 Git clone 对跳板带宽和并发连接的影响。正式容量和 HA 由部署运维接管,但工具侧要保留可观测入口和失败边界。
永久公钥与短期 SSH 证书各有成本
小团队用平台账号公钥最直接,但设备和人员增加后,清单、轮换和离职回收容易失控。SSH CA/短期证书能集中控制 principal 和有效期,却引入签发服务、CA 私钥保护、时钟和应急回退。是否采用证书体系,应由账号规模、合规审计和平台支持度决定,不因“更高级”而默认上马。
旧算法兼容不能靠永久降级解决
旧自托管服务可能只支持已不推荐的 key 或签名算法。临时在单个 Host 上放宽算法也应有 owner、到期日和升级计划,不能在 Host * 全局开启。优先升级服务端;客户端兼容开关只是受控过渡,不是最终架构。
安装后
ssh -V、ssh-keygen、ssh-add 可用,实际可执行文件来源明确。终端、IDE、Git Bash、Windows OpenSSH 或 WSL 没有无意混用不同客户端和 agent。私钥有 passphrase,文件权限受控;平台只收到 .pub。
ssh-add -l 与本地公钥指纹一致。
配置后
具体 Host 在通配默认项之前。每个账号使用独立别名、IdentityFile 和 IdentitiesOnly yes。ssh -G 展开的主机、用户、身份和代理符合预期。
known_hosts 指纹来自平台官方或企业可信渠道,没有关闭主机校验。
项目接入时
origin 使用正确 SSH 别名,没有误用个人账号或真实域名绕过配置。ssh -T 显示预期平台账号;非零退出码已按平台文档解释。git ls-remote --exit-code origin HEAD 能读取目标仓库。
测试仓库能创建并删除临时分支,保护分支仍保持门禁。
代理与团队推广前
跳板和目标分别配置身份、主机指纹、权限与审计。没有全局 ForwardAgent yes,确需转发的主机经过风险评审。ProxyJump 与 ProxyCommand 没有冲突,代理模板已按目标系统验证。
有主机密钥轮换、个人 key 轮换、丢机、离职和紧急撤销流程。个人 key、部署 key、机器人 key 与 CI 身份互不混用。旧算法例外和长期公钥都有 owner、复查日与退出计划。
