Git 凭证安全迁移实战——从 HTTPS Token 到 SSH 密钥的完整加固
一句话总结
一次 Git 用户迁移操作,意外暴露了 HTTPS Token 明文存储的安全隐患,最终通过 SSH ED25519 密钥 + git filter-branch 历史重写 + 多层清理,完成从「裸奔」到「可控」的凭证安全加固。
背景:一次看似简单的用户切换
团队接手了一个已有 Git 仓库的招采网络投标系统项目,代码托管在 GitHub。项目本身已经完成了 5 次历史提交,但在检查配置时发现了一个问题:所有历史提交的作者都是上一个维护者的邮箱 old-user@company.com,而我们需要将所有提交归属切换到新的 GitHub 账号下。
这看起来像一个简单的 git config 操作,实际上却引发了一连串安全问题的暴露。
第一阶段:切换用户配置
变更 Git 本地身份
1 | git config user.name "new-user" |
这不是什么复杂操作。两条命令执行后,后续的 commit 都会带上新的身份信息。
问题来了:历史提交怎么办?
1 | $ git log --all --format='%an %ae' |
5 条历史提交,全是旧身份。如果只是继续提交,仓库里会永远保留这两个人的混合记录。对于代码审计和归属确认来说,这是不可接受的。我们需要彻底清除旧用户的所有痕迹。
第二阶段:重写 Git 历史
git filter-branch —— 一把手术刀
1 | git filter-branch -f --env-filter ' |
这个命令遍历了仓库的每一条 commit,把作者和提交者信息全部替换为新账号。-f 是因为之前执行过一次需要强制覆盖备份,--all 确保覆盖所有分支。
清理 filter-branch 的备份引用
执行完毕后,git log --all 仍然显示旧记录。原因在于 filter-branch 会在 refs/original/ 下保留原始引用的备份:
1 | $ git for-each-ref --format='%(refname) %(authoremail)' refs/original/ |
这些备份引用必须手动清除:
1 | # 删除备份引用 |
验证结果:
1 | $ git log --all --format='%an %ae' |
至此,历史重写完成,旧用户的所有痕迹从 Git 对象数据库中彻底抹除。
第三阶段:HTTPS Token 推送失败——安全隐患浮出水面
症状:push 超时
准备强制推送到 GitHub 时,遇到了一个奇怪的问题:
1 | $ git push --force origin main |
使用 curl 验证网络和认证:
1 | # Token 本身有效 |
这说明问题不在 Token 本身,而在网络层面。
切换到 SSH:更安全的替代方案
与其Debug网络问题,不如借机升级认证方式。HTTPS Token 认证有几个固有问题:
| HTTPS Token 的缺陷 | SSH 密钥的优势 |
|---|---|
Token 明文存在 ~/.git-credentials |
私钥存在 ~/.ssh/,可设密码保护 |
| 泄露后攻击者无需任何额外因素即可操作 | 私钥文件权限 600 阻止其他用户读取 |
Token 权限通常是 repo 全范围 |
每个密钥可绑定不同账号,权限隔离 |
| 通过网络传输(虽然 HTTPS 加密) | 基于公私钥挑战-应答,私钥从不出网 |
第四阶段:SSH 密钥配置踩坑全记录
生成新的 ED25519 密钥
1 | ssh-keygen -t ed25519 -C "new-user@example.com" -f ~/.ssh/id_ed25519_github |
选择 ED25519 而非 RSA 的理由:
- 安全性:256 位 ED25519 ≈ 3072 位 RSA,但抗侧信道攻击能力更强
- 性能:密钥生成和签名验证比 RSA 快一个数量级
- 体积:公钥仅 68 字符,私钥仅 48 字节,比 RSA 短得多
将公钥 ~/.ssh/id_ed25519_github.pub 添加到 GitHub → Settings → SSH and GPG keys。
坑:旧密钥抢答
验证 SSH 连接时出现了意外结果:
1 | $ ssh -T git@github.com |
明明配的是新密钥,为什么认证成了另一个账号?因为 SSH 客户端默认会逐个尝试 ~/.ssh/ 下的所有私钥,而之前存在一个 id_rsa 私钥,它被优先匹配了。
解决方法:~/.ssh/config + IdentitiesOnly
1 | # GitHub 专用密钥配置 |
关键参数说明:
| 参数 | 作用 |
|---|---|
IdentitiesOnly yes |
禁止 SSH 使用 config 未列出的密钥文件,强制只使用 IdentityFile 指定的密钥 |
IdentityFile |
精确指定该 Host 使用的私钥路径 |
坑:Shell ~ 路径转义
在自动化配置中,如果用代码写入 ~/.ssh/config,注意 ~ 可能不会被 Shell 展开。这次就遇到了:配置文件被写入了项目目录下的 ./~/.ssh/config,而不是 $HOME/.ssh/config。
教训:在脚本中始终使用 $HOME 而非 ~:
1 | # 错误写法 |
确认配置正确后:
1 | $ ssh -T git@github.com |
切换远程仓库 URL
HTTPS → SSH 的远程地址格式:
1 | # 从 HTTPS |
第五阶段:强制推送与最终清理
强制推送
1 | git push --force origin main |
⚠️ --force 是危险操作——它会用本地历史覆盖远程历史。如果有协作者基于旧的远程历史工作,他们的本地仓库会出现冲突。在工业环境中,更推荐 --force-with-lease:
1 | # 更安全的强制推送:如果远程有本地不知道的新提交,拒绝推送 |
credential.helper 清理
最容易被忽略的一步——清除残留在 Git 配置中的凭证助手:
1 | # 检查当前配置 |
最终验证
1 | # 1. 确认远程 URL 是 SSH |
工业环境中的 Git 凭证管理最佳实践
1. 永远不要使用 HTTPS Token + credential.helper store
credential.helper = store 会把 Token 明文写入 ~/.git-credentials 文件。任何能读取该文件的程序或用户都能获取完整的仓库操作权限。
1 | # 这等于把钥匙放在门垫下 |
本次事件中,
~/.git-credentials文件内容被误删后推送失败,从反面证明了这个 Token 确实是唯一的认证凭据——但也同时证明了任何接触到文件的人都可以用它操作仓库。
2. 使用 Token 时的最少权限原则
如果必须使用 Token(CI/CD 环境),务必配置最小 Scope:
| Scope | 何时需要 | 不需要时关闭 |
|---|---|---|
repo |
读/写代码和 PR | — |
workflow |
触发 GitHub Actions | 纯代码推送不需要 |
admin:public_key |
管理 SSH 密钥 | ❌ 永远不要给 CI |
delete_repo |
删除仓库 | ❌ 绝对不要给 |
查看当前 Token 权限:
1 | curl -s -I -H "Authorization: token ghp_xxxxx" https://api.github.com/user | grep x-oauth-scopes |
3. CI/CD 环境中的凭证管理
| 环境 | 推荐方案 |
|---|---|
| GitHub Actions | 使用内置 secrets.GITHUB_TOKEN,自动过期,无需手动管理 |
| Jenkins | 使用 Credentials Plugin,不要在 pipeline 脚本中硬编码 |
| Docker 构建 | 使用 --secret 挂载,避免 Token 残留到镜像层 |
| 开发机 | SSH 密钥 + IdentitiesOnly yes,不用 credential.helper |
4. SSH 密钥的安全加固清单
1 | # ① 为私钥设置密码短语 |
5. 历史重写:filter-branch vs git-filter-repo
git filter-branch 虽然能用,但 Git 官方已经不推荐使用它:
| 维度 | filter-branch | git-filter-repo |
|---|---|---|
| 速度 | 慢(逐 commit 遍历) | 快(批量操作) |
| 安全性 | 可能产生损坏的历史 | 严格验证 |
| 备份清理 | 需手动 update-ref -d |
自动清理 |
| 维护状态 | 基本冻结 | 活跃维护 |
现代替代方案:
1 | # 安装 |
6. 定期凭证轮换
- Personal Access Token:每 30~90 天轮换一次
- SSH 密钥:每 6~12 个月轮换,离职人员密钥立即吊销
- GitHub Action Secrets:随项目阶段(开发/预发/生产)使用不同权限的 Token
学习路线
1 | Git 用户配置 |
总结
这次操作从「换个用户名」开始,逐步暴露了 Git 认证链路中的多个安全死角:
- 明文 Token 存储 —
~/.git-credentials是最容易被忽略的泄露点 - 历史提交的身份归属 —
git filter-branch能解决,但git-filter-repo更好 - SSH 密钥的选择和隔离 — ED25519 +
IdentitiesOnly是最佳实践 - 清理远比配置重要 — 配置完 SSH 后删除 Token、清除 credential.helper 才是安全闭环
对于任何一个接手已有仓库的开发者来说,这套流程不仅是「改个配置」,而是一次完整的凭证安全检查——你永远不知道上一个维护者在系统里留下了哪些可以操作的入口。

