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