☰
Git多平台SSH配置:Gitee与GitHub双账号身份隔离及排错实践
2026/10/27 21:07:32 网站建设 项目流程

一台开发机上同时挂着 Gitee 和 GitHub 的仓库,几乎是现在做开发的默认状态:单位内部协作走一个平台,开源项目和自娱自乐的小工具走另一个。真到动手配置那天,十个人里得有八个卡在Permission denied (publickey)这一行前面,接着开始满世界找"重新生成密钥"的教程,把本来还能用的 key 覆盖一遍,最后两个平台一起挂掉。

这套配置我换过好几台机器,从 Linux 到 Windows 都跑过,基本是复制粘贴就能用的状态。之所以值得单独写一篇,是因为大部分教程只告诉你"生成两个 key 写进 config",却没讲清楚为什么要这么写、少写一行会怎样、报错之后从哪里查。下面按"传输身份、提交身份、仓库身份"三层拆开讲,新手可以照着抄,踩过坑的也能对上号。

1. 冲突不在密钥数量,而在三条身份线被混成了一条

1.1 SSH 认证的默认行为:客户端根本不知道你在给谁发请求

很多人对 SSH 有个误解,以为客户端会"识别"当前连的是哪个平台,然后挑对应的钥匙。事实完全相反:ssh拿到一个主机地址之后,只会按固定顺序把手上所有的私钥挨个递过去试一遍——先~/.ssh/id_rsa,再id_ecdsa,再id_ed25519,最后是 ssh-agent 里缓存的所有身份。服务端拿自己那边登记过的公钥逐个比对,对上号就放行,对不上就继续,全试完还不行就甩给你一句Permission denied (publickey)。

理解这一点非常关键,因为它直接推翻了"必须一个平台一把钥匙"的假设。理论上你把同一个公钥同时登记到两个平台,一把钥匙开两把锁,完全能跑通,我早期就是这么干的。真正让人抓狂的问题出在后面:当机器上同时存在多个私钥时,客户端会一股脑全递出去,而服务端对认证尝试次数有上限(常见是 6 次),超过就直接断开,日志里写的却是Too many authentication failures,看起来跟权限毫无关系。

1.2 换密钥不会改变提交作者,这是最大的认知错位

还有一类问题更有迷惑性:有人为了让 Gitee 的提交显示成 A 身份、GitHub 的提交显示成 B 身份,特意换了 SSH 密钥,结果发现git log里的作者压根没变。原因很简单——SSH 密钥只负责"你能不能推上去",提交作者是另一套东西。

user.name和user.email会在你执行git commit的那一刻写进 commit 对象里,属于仓库内容的一部分,跟网络传输层没有任何关系。你就算用 Gitee 的 key 把仓库推到 GitHub,commit 里的 author 依然是当前生效的user.email。这两条线必须分开配置,混在一起想,怎么配都是错的。

我把这三层身份整理成一张表,后面所有配置都是围绕它展开的:

身份层由什么决定影响范围出问题的表现
传输身份SSH 私钥 / HTTPS 凭据能否连上远端Permission denied、连接超时
提交身份user.name/user.email写进 commit 元数据提交作者串号、贡献图不统计
仓库身份remote.origin.url推到哪个地址推错仓库、两个平台互相覆盖

1.3 三种可选形态,先选路线再动手

在动手之前先想清楚要哪种形态,不然配置到一半改主意,返工成本很高。

第一种是一把钥匙两把锁:只保留一个id_ed25519,把公钥同时贴到两个平台。优点是配置量最小,什么都不用改;缺点是一旦私钥泄露,两个平台的仓库同时暴露,而且没法按平台做细粒度权限管控。个人玩具项目这么搞没问题,涉及工作仓库就不太合适了。

第二种是一锁一匙加 Host 别名:两个平台各生成一把,通过~/.ssh/config里的别名把请求分流。这是我推荐的主力方案,隔离性好,排查时也容易定位。

第三种是目录隔离加条件配置:在第二种基础上,再通过includeIf让不同目录下的仓库自动套用不同的提交身份。适合工作仓库和个人仓库混在同一台机器上的场景。

下面按第二、第三种方案展开,第一种方案只在排错部分作为对照出现。

2. 生成两把互不干扰的密钥,先把参数讲明白

2.1 ssh-keygen 的每个参数都不是摆设

先把两条命令摆出来,然后逐个解释:

ssh-keygen -t ed25519 -C "github-main" -f ~/.ssh/id_ed25519_github -N "" ssh-keygen -t ed25519 -C "gitee-main" -f ~/.ssh/id_ed25519_gitee -N ""

-t ed25519指定算法。选它是因为密钥短、握手快,同等安全强度下比 RSA 省事得多。老一点的服务器环境如果明确不支持,再退回-t rsa -b 4096,但这年头基本遇不到了。

-C是注释,它会原样追加到公钥文件末尾。它不参与任何认证逻辑,纯粹是给人看的标签。所以这里别写邮箱,写用途标签更实用——邮箱会变,用途不会。有些平台在密钥列表里会显示这段注释,写上github-main一眼就能认出是哪台机器的。

-f指定输出路径。这一项是整套方案的地基:两把钥匙必须落在不同文件里,默认的id_ed25519路径只够放一把。

-N ""是把口令设为空,让它全程无交互。这在脚本里方便,但也意味着私钥文件一旦被拿走就等于裸奔。我的实际做法是交互式生成、设置口令、然后交给 ssh-agent 缓存,登录后输一次,之后整个会话内都不用再输。安全性比空口令高一个量级,麻烦程度也就多敲一次密码。

生成完你会得到四个文件:两个私钥(无后缀)、两个公钥(.pub)。私钥绝对不能外传,公钥才是要贴到平台上去的东西。这一点每年都有新人搞反。

2.2 上架公钥时要做的两个动作

第一个动作是把.pub的内容复制出来。别用文本编辑器打开——有些编辑器会顺手改编码或者加 BOM,粘上去就认不出来。命令行复制更保险:

# macOS pbcopy < ~/.ssh/id_ed25519_github.pub # Windows clip < %USERPROFILE%\.ssh\id_ed25519_github.pub # Linux xclip -selection clipboard < ~/.ssh/id_ed25519_github.pub

拿到对应平台的密钥管理页面粘贴,标题随便起个能认出来的名字,比如"办公台式机-2024"。这里有个容易被忽略的点:粘贴内容前后不要有多余空行,某些平台的校验比较严格,多一个换行就报格式错误,你还会以为是密钥生成坏了。

第二个动作经常被跳过:首次连接时核对主机指纹。ssh第一次连陌生主机会弹一句Are you sure you want to continue connecting,很多人看都不看就敲 yes。指纹是要核对的,各平台的官方文档里都能查到当前使用的主机公钥指纹,对得上再信任。指纹不对意味着中间可能有人在冒充,这种场景下敲 yes 等于把凭据直接送出去。

核对通过之后,指纹会被写进~/.ssh/known_hosts,后续连接不再询问。如果哪天平台轮换了主机密钥,你会看到一大段WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED的警告——这时候先别急着ssh-keygen -R删记录,去官方公告确认一下是不是真的轮换了。

2.3 权限位和 ssh-agent,两个不报错但会卡死你的前置条件

Linux 和 macOS 上,ssh对文件权限极其挑剔。~/.ssh目录必须是700,私钥必须是600,config建议600,公钥644即可。权限宽了会直接拒绝使用这个密钥,报错信息还特别含糊,只说bad permissions。

chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519_github ~/.ssh/id_ed25519_gitee ~/.ssh/config chmod 644 ~/.ssh/*.pub

Windows 上这套命令不生效,但 OpenSSH for Windows 会检查 NTFS ACL。最省事的做法是把密钥放在用户目录下的.ssh里,让它自然继承用户级权限,不要手动去改 ACL。如果确实遇到权限报错,用管理员权限的 PowerShell 执行icacls修复,或者在"属性-安全-高级"里把继承关系恢复,比在命令行里绕圈快得多。

ssh-agent 的作用是把解密后的私钥缓存在内存里,避免每次操作都输口令:

eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519_github ssh-add ~/.ssh/id_ed25519_gitee ssh-add -l # 确认已加载的身份

提示:macOS 上如果想让 agent 记住口令跨重启,在~/.ssh/config顶部加Host *段落写AddKeysToAgent yes和UseKeychain yes。Linux 桌面环境一般由 keyring 自动接管,手动执行一次ssh-add就够。

这里埋着一个陷阱,下一节会展开:agent 里的身份越多,握手时被递出去的钥匙就越多,越容易撞上服务端的尝试次数上限。

3. ~/.ssh/config 是整套方案的分流器

3.1 最小可用配置长什么样

这个文件默认不存在,手动创建即可。下面是能直接跑的最小版本:

Host gh HostName github.com User git IdentityFile ~/.ssh/id_ed25519_github IdentitiesOnly yes Host gt HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_gitee IdentitiesOnly yes

Host是你在命令行里书写的别名,HostName是真实域名。这两个分开写,是为了让后面的 git 地址能明确表达"走哪条通道"。User对两个平台来说都是固定的git,不是你的登录名,写错了会直接认证失败。IdentityFile指定该通道专用的私钥。

注意缩进用空格,不要用 Tab。虽然新版 OpenSSH 容忍 Tab,但跨版本、跨编辑器迁移时,Tab 被展开成不同宽度引发的问题排查起来极其浪费时间,索性统一用四个空格。

这个文件遵循"首个匹配生效"原则,所以Host *这种通配段落一定要放在最底下,否则会吃掉上面所有的具体配置。

3.2 IdentitiesOnly 这一行,决定了你会不会撞上 MaxAuthTries

IdentitiesOnly yes是整份配置里最重要的一行,也是最常被抄漏的一行。

不写它的后果是这样的:ssh会先按IdentityFile试一次,如果这次因为某种原因没成(比如密钥还没生效、或者服务端出于策略拒绝),它会继续把 agent 里的其他身份全部递上去。机器上装了三四个项目的密钥时,6 次尝试额度瞬间用完,最终报出来的错误是Too many authentication failures。

这个报错极具误导性,因为它听起来像是"尝试太多被风控了",而真实原因是"你递错了钥匙"。加上IdentitiesOnly yes之后,ssh就只使用IdentityFile指定的那一把,不再四处乱试,错误信息立刻变得干净可读。

注意:如果你同时在Host段里写了IdentityFile又用了ssh-agent,IdentitiesOnly配合AddKeysToAgent是没问题的;但如果你依赖 agent 自动挑选,那就别写IdentitiesOnly,两者思路相反,别混着来。

3.3 别名写法让 remote 地址自己说明走哪条通道

配置好之后,克隆命令就从这样:

git clone git@github.com:yourname/demo.git

变成这样:

git clone git@gh:yourname/demo.git

看起来只是两个字母的差别,实际收益很大。第一,看地址就知道走哪条通道、用哪把钥匙,排查时不用翻配置。第二,同一个平台上如果有多个账号(比如一个工作号一个私人号),github.com这个 Host 只能对应一把钥匙,而Host gh-work和Host gh-personal可以各配一把,互不干扰。这一点是纯别名写法真正的价值所在。

有人会担心git@gh:xxx这种地址发给别人打不开——确实打不开,但 remote 地址本来就只对本地生效,协作时对方用自己的配置即可。真要对外分享,复制网页上给的原始地址就行。

3.4 ssh -T 的输出怎么读

配完先别急着 clone,用测试命令验证:

ssh -T git@gh ssh -T git@gt

成功的输出长这样(各平台文案略有差异):

Hi yourname! You've successfully authenticated, but GitHub does not provide shell access.

看到successfully authenticated就是通了,后面那句"不提供 shell"是正常的,两个平台都不给交互式 shell,不用管。

如果是Permission denied (publickey),说明钥匙没对上,别急着重生成,先加-v看详细过程:

ssh -vT git@gh 2>&1 | grep -E "Offering|Server accepts|Authenticated|identity file"

输出里Offering public key后面跟的路径,就是它实际递出去的钥匙。如果这个路径不是你期望的那个,问题在config里;如果路径对了但服务端还是不认,问题在平台侧的密钥登记,大概率是复制时漏了字符或者贴到了错误的账号下。

4. Git 身份层:三档配置的优先级与自动化

4.1 全局、目录、仓库,谁覆盖谁

git config有三个作用域,优先级从低到高:

git config --system # 系统级,一般不用碰 git config --global # 用户级,写在 ~/.gitconfig git config --local # 仓库级,写在 .git/config git config --worktree # 工作区级,多工作树场景才用

高优先级覆盖低优先级。也就是说,仓库里一旦有本地配置,全局配置对它就失效了。

查询当前生效值和它的来源,最实用的命令是:

git config --show-origin --get user.email

输出会明确告诉你这个值是哪个文件提供的,比如file:/home/you/.gitconfig work@example.com。排查"提交作者为什么不对"时,这一条命令能省掉半小时瞎猜。

4.2 includeIf 按目录自动切换身份

工作仓库和个人仓库混在一起时,每次都手动git config --local太累,也容易忘。includeIf可以在进入某个目录时自动加载另一份配置文件。

主配置~/.gitconfig里写:

[user] name = yourname email = personal@example.com [includeIf "gitdir:~/work/"] path = ~/.gitconfig-work

~/.gitconfig-work里只写需要覆盖的部分:

[user] name = yourname email = work@example.com

这样~/work/下的所有仓库自动用工作邮箱,其他位置用个人邮箱。

这地方有三个必踩的坑,我一个个说。

坑一:路径末尾的斜杠不能省。gitdir:~/work/表示"work 目录及其子目录",写成gitdir:~/work则只匹配 work 这个路径本身,下面的仓库全都不生效。少了这一个字符,配置看起来完全正确,就是不工作。

坑二:大小写。macOS 和 Windows 的文件系统默认不区分大小写,Git 的匹配却是区分的。目录叫Work、配置写work,在那边就是不匹配。稳妥做法是用gitdir/i:前缀,强制忽略大小写。

坑三:相对路径的基准。path用相对路径时,基准是.gitconfig所在目录,不是当前工作目录。我一般直接写绝对路径,省得推演。

4.3 hasconfig 条件:按远端地址匹配更精准

Git 2.36 之后多了hasconfig:remote.*.url条件,可以按远端地址自动切身份,比按目录匹配更贴合实际意图:

[includeIf "hasconfig:remote.*.url:git@gh:**"] path = ~/.gitconfig-github

意思是"只要这个仓库的某个 remote 地址匹配git@gh:**,就加载这份配置"。这样即使仓库放在任意目录,只要 remote 指向 GitHub 别名,身份就自动切过去。

不过它有个使用限制要提前知道:克隆操作时这个条件不生效,因为刚git init出来的空仓库还没有 remote。所以用这种方式,新克隆的仓库第一次提交前需要手动确认一下身份,或者先配好 remote 再补配置。另外某些 Git 版本在读取这种条件时会二次解析配置文件,改动之后用git config --show-origin --get user.email复验一遍最保险。

4.4 提交之后怎么确认作者没串

三道检查,从提交前到提交后:

git var GIT_AUTHOR_IDENT # 提交前,看即将使用的身份 git log -1 --format='%an <%ae>' # 提交后,看实际写入的作者 git log --format='%ae' | sort -u # 看整个仓库出现过哪些邮箱

第三条在接手别人仓库时特别有用,能一眼看出历史提交里混了几个身份。如果只是想修最近一次提交的作者,git commit --amend --reset-author配合正确的本地配置即可;要改历史提交就得动用 rebase,那是另一件事了,改动已推送的历史之前务必确认没有人基于它工作。

5. 仓库层:克隆、改写、双推的完整链路

5.1 克隆时地址怎么选

新仓库起步最简单,直接用别名地址:

git clone git@gh:yourname/demo.git git clone git@gt:yourname/demo.git

克隆下来之后,git remote -v里显示的 URL 就是带别名的形式,之后所有fetch、push都会自动走对应通道,不需要额外配置任何东西。别名机制的好处在这里体现得最明显——remote 地址自己携带了路由信息,人和工具都能读懂。

如果你不想改地址、还是想用git@github.com:xxx的原生形式,也能工作,只要在config里用Host github.com而不是Host gh。两种写法本质相同,区别只在于你想不想留出多账号的扩展空间。我个人的习惯是能留就留,改地址的成本远低于后来加账号时返工的成本。

5.2 已有仓库的地址改写,不影响任何历史

已经克隆下来的仓库要换通道,改 remote 地址即可,历史提交、分支、标签全都不受影响:

git remote set-url origin git@gh:yourname/demo.git git remote -v # 确认改完

这里有个常见的心理障碍:很多人担心改 remote 地址会不会导致提交记录丢失或者重新关联。不会的。remote 只是一个"远端书签",跟本地对象库毫无关系,改它就跟改个快捷方式一样。

另一种更省事的做法是在全局配置里做 URL 重写:

[url "git@gh:"] insteadOf = git@github.com:

这样所有写git@github.com:的地址在传输时会被自动替换成git@gh:。听起来很美,但我实际用下来不太推荐,理由是这个替换是全局生效的,出问题时你会在两个地方之间来回找——地址里明明写的是 github.com,实际走的却是别名通道,ssh -v打出来的日志跟命令行的地址对不上,排查成本陡增。真要批量处理,宁可写个脚本跑一遍git remote set-url,改完的地址是显式的,一目了然。

5.3 一个仓库同时推两个平台

把自己的小项目在两边各放一份,是很常见的需求。做法是先设好 fetch 地址,再追加 push 地址:

# 先设置拉取地址 git remote set-url origin git@gh:yourname/demo.git # 追加两个推送地址 git remote set-url --add --push origin git@gh:yourname/demo.git git remote set-url --add --push origin git@gt:yourname/demo.git git remote -v # 此时应看到一个 fetch、两个 push

顺序很重要:必须先设好基础的 fetch 地址,再用--add --push追加。如果顺序反了或者漏了第一步,会出现git push能跑但git pull报"仓库不存在"的诡异状态,因为 origin 压根没有可用的 fetch 地址。

追加完成之后,一次git push origin main会依次推到两个平台。它的行为是串行的,第一个失败就会中断,后面的不执行。所以如果其中一个平台偶发连不上,你会看到一半成功一半失败的状态。这种时候单独推另一个平台就行:

git push git@gt:yourname/demo.git main

用完整地址推送是合法的,Git 会临时解析成一次推送目标,不依赖 remote 配置。

提示:如果两个平台的分支保护策略不同(比如一边要求 PR、一边允许直推),双推很容易在一边失败。我自己的做法是给主推平台保留origin,另一个平台单独加一个名为mirror的 remote,需要时手动执行git push mirror main,不设自动双推。可控性比"一次推俩"重要得多。

6. 排查链路:从报错信息倒推根因

6.1 Permission denied 的四层排查

这句报错几乎覆盖了所有认证失败场景,只盯着它看是没用的,必须分层定位。我固定的排查顺序是这样:

排查层检查命令判断依据
密钥文件ls -l ~/.ssh私钥存在、权限 600
配置命中ssh -G git@gh | grep identityfile输出的是期望路径
实际递出ssh -vT git@ghOffering public key路径正确
平台侧网页端密钥列表公钥内容一致、账号正确

ssh -G这条命令知道的人不多,它不发起连接,只打印最终解析出来的配置。用它确认identityfile、user、hostname三个值,能立刻判断是配置没生效还是配置生效了但密钥不对——很多人配置写在了错的文件里(比如写进了~/.ssh/config.local),或者Host段前面有语法错误导致整段被跳过,用-G一看便知。

平台侧的问题占比其实不低。最常见的是公钥贴到了另一个账号下,或者粘贴时首尾带了空格。这种情况下本地怎么查都是对的,只有登录网页端逐字比对才能发现。我的习惯是贴完之后立刻在页面上把内容复制回来,和本地.pub文件做一次 diff:

# 把网页上复制的内容存成 remote.pub,然后比对 diff <(tr -d '\n' < ~/.ssh/id_ed25519_gh.pub) <(tr -d '\n' < remote.pub)

长度对不上、中间有差异,问题就锁定了。

6.2 提交作者串号的三种典型成因

第一种是全局配置盖过了一切。检查git config --show-origin --get user.email,如果来源是~/.gitconfig而你以为配了本地,那就是没配成功。

第二种是includeIf 的路径没匹配上。前面说过的斜杠、大小写两个坑,九成问题出在这儿。验证方法很简单,在目标目录下执行git config --get user.email,看输出对不对,不对再回头检查路径。

第三种是仓库里残留了旧的本地配置。接手别人的仓库、或者之前手动git config --local设过,之后忘了。用git config --local --list一眼就能看到,多余的直接git config --local --unset user.email删掉,让它回落到全局或 includeIf 的值。

还有一个隐蔽的变种:user.useConfigOnly。这个开关设成true之后,Git 在找不到明确配置时会拒绝提交,而不是用默认的机器名拼一个邮箱。它算是个好习惯,能防止你在无意识的情况下提交一个乱七八糟的作者信息,配置方式是git config --global user.useConfigOnly true。

6.3 端口不通时的兜底路径

有些网络环境对 22 端口不友好,表现是ssh -T一直挂着然后超时,跟密钥完全无关。这种时候先确认是不是端口问题:

ssh -T -p 22 git@gh -o ConnectTimeout=5

如果 5 秒内直接超时,基本可以确定是端口层面被拦了。多数代码托管平台都提供了备用入口端口,具体的主机名和端口号以各平台官方文档为准,配上之后:

Host gh HostName ssh.<平台官方给出的备用域名> Port 443 User git IdentityFile ~/.ssh/id_ed25519_github IdentitiesOnly yes

这样走的是官方支持的备用通道,跟平常的 SSH 完全等价,密钥、config 都不需要改。

另一条兜底路线是改用 HTTPS 协议加凭据管理器。SSH 走不通时,HTTPS 通常还有戏,代价是要管理访问令牌。Git 自带的凭据管理器能把这套东西存进系统钥匙串,配置一次就不用管了:

git config --global credential.helper manager # Windows git config --global credential.helper osxkeychain # macOS git config --global credential.helper libsecret # Linux 桌面

用 HTTPS 时,提交身份和传输身份彻底解耦这件事反而更直观——凭据管的是"能不能推",user.email管的还是"谁提交的",两套东西各管各的,不会再混。

6.4 几个不报错但会恶心人的细节

换行符。Windows 上不加配置,Git 可能把 LF 转成 CRLF 提交上去,导致整个文件显示全被修改。跨平台协作的仓库建议统一:

git config --global core.autocrlf input # macOS / Linux git config --global core.autocrlf true # Windows

仓库里有.gitattributes的话以它为准,那个优先级更高。

配置文件的编码。~/.ssh/config必须是纯 ASCII 或者 UTF-8 无 BOM。用某些编辑器保存时会带上 BOM,ssh读到第一个字节就报语法错误,而且报错行号指向第一行,看起来像是Host写错了,实际是文件头有问题。改完配置先ssh -G跑一下,能解析就说明文件本身没毛病。

同名 Host 段重复。配置文件是追加式维护的,时间长了很容易出现两段Host gh。SSH 的处理方式是"首个匹配生效",也就是先出现的那段赢,后面那段被静默忽略。你会发现自己改了半天没反应,因为改的是被忽略的那一段。维护这个文件时养成一个习惯:改完grep -n "^Host" ~/.ssh/config看一眼有没有重名。

~的展开。IdentityFile里写~/.ssh/xxx在绝大多数环境下没问题,但某些 Windows 的 Git Bash 组合下展开会出错。稳妥起见写绝对路径,或者用%d占位符表示用户主目录,具体写法查一下所用版本的文档。

7. 把配置沉淀成脚本,换机器时不用重来一遍

7.1 一份能直接跑的初始化脚本

手工敲两遍命令还行,第三台机器上就会开始漏步骤。我现在的做法是留一个脚本,新机器上跑一次,剩下的只需手动贴公钥:

#!/usr/bin/env bash set -euo pipefail SSH_DIR="$HOME/.ssh" mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" gen_key() { local tag="$1" local keyfile="$SSH_DIR/id_ed25519_${tag}" if [ -f "$keyfile" ]; then echo "[skip] $keyfile 已存在,不覆盖" return fi ssh-keygen -t ed25519 -C "${tag}-$(hostname)-$(date +%Y%m)" -f "$keyfile" chmod 600 "$keyfile" chmod 644 "${keyfile}.pub" echo "[ok] 已生成 $keyfile" } gen_key github gen_key gitee CONFIG="$SSH_DIR/config" touch "$CONFIG" chmod 600 "$CONFIG" add_host_block() { local alias="$1" hostname="$2" keyfile="$3" if grep -qE "^Host[[:space:]]+${alias}$" "$CONFIG"; then echo "[skip] Host ${alias} 已存在" return fi cat >> "$CONFIG" <<EOF Host ${alias} HostName ${hostname} User git IdentityFile ${keyfile} IdentitiesOnly yes EOF echo "[ok] 已写入 Host ${alias}" } add_host_block gh github.com "$SSH_DIR/id_ed25519_github" add_host_block gt gitee.com "$SSH_DIR/id_ed25519_gitee" echo "---- 待上架的公钥 ----" echo "GitHub:" cat "$SSH_DIR/id_ed25519_github.pub" echo "Gitee:" cat "$SSH_DIR/id_ed25519_gitee.pub"

脚本里有几个刻意的设计。set -euo pipefail保证任何一步失败立刻停下,不会带着半截配置继续跑。gen_key里先判断文件是否存在再生成,是为了防手滑——真要重生成,手动删了再跑,比让脚本自动覆盖安全得多。add_host_block用grep判重,避免重复执行时把config写成一堆重复段落。注释里带上主机名和年月,将来清理旧密钥时一眼能看出是哪台机器什么时候配的。

跑完之后把输出的两段公钥分别贴到对应平台,然后ssh -T验证,整个流程就闭环了。

7.2 换机器时的迁移清单

新环境上手,按这个顺序走一遍基本不会漏:

  1. 确认~/.ssh目录存在且权限为 700
  2. 生成或从安全渠道取回两把私钥,权限 600
  3. 把公钥贴到对应平台,逐字比对内容
  4. 写好~/.ssh/config,用ssh -G验证解析结果
  5. 首次连接核对主机指纹,确认后写入known_hosts
  6. ssh -T分别验证两条通道
  7. 配置全局user.name/user.email
  8. 需要目录隔离的话,加includeIf并验证生效
  9. 克隆一个测试仓库,走一遍完整提交推送
  10. 用git log --format='%an <%ae>'确认提交作者正确

这个清单我一般会存成仓库里的一个docs/env-setup.md,跟着项目走。换机器的时候照着勾一遍,比凭记忆现想靠谱得多。

最后分享一个我自己的习惯:每次改完 SSH 配置,先跑ssh -G,再跑ssh -T,最后才碰 git。这三步分开做的原因是,前两步出问题都好定位,一旦混进 git 的报错里,注意力就会被拉偏。真到了git push报错那一步,问题基本已经不在 SSH 层了,多半是地址写错、分支不存在或者权限不足,看一眼报错的完整输出就能分辨。分层验证虽然多花半分钟,但省下来的排查时间是以小时计的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询