---
title: Git 凭证安全迁移实战——从 HTTPS Token 到 SSH 密钥的完整加固
date: 2026-06-12 19:00:00
tags: [Git, SSH, 安全, DevOps, 凭证管理, 历史重写]
categories:
- 工具-部署
---

# 一句话总结

> 一次 Git 用户迁移操作，意外暴露了 HTTPS Token 明文存储的安全隐患，最终通过 SSH ED25519 密钥 + git filter-branch 历史重写 + 多层清理，完成从「裸奔」到「可控」的凭证安全加固。

---

# 背景：一次看似简单的用户切换

团队接手了一个已有 Git 仓库的招采网络投标系统项目，代码托管在 GitHub。项目本身已经完成了 5 次历史提交，但在检查配置时发现了一个问题：**所有历史提交的作者都是上一个维护者的邮箱** `old-user@company.com`，而我们需要将所有提交归属切换到新的 GitHub 账号下。

这看起来像一个简单的 `git config` 操作，实际上却引发了一连串安全问题的暴露。

---

# 第一阶段：切换用户配置

## 变更 Git 本地身份

```bash
git config user.name "new-user"
git config user.email "new-user@example.com"
```

这不是什么复杂操作。两条命令执行后，后续的 commit 都会带上新的身份信息。

## 问题来了：历史提交怎么办？

```bash
$ 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.com
```

5 条历史提交，全是旧身份。如果只是继续提交，仓库里会永远保留这两个人的混合记录。对于代码审计和归属确认来说，这是不可接受的。我们需要**彻底清除旧用户的所有痕迹**。

---

# 第二阶段：重写 Git 历史

## `git filter-branch` —— 一把手术刀

```bash
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/` 下保留原始引用的备份：

```bash
$ 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
```

这些备份引用必须手动清除：

```bash
# 删除备份引用
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
```

验证结果：

```bash
$ git log --all --format='%an %ae'
new-user new-user@example.com  ← 仅剩一条干净记录
```

至此，历史重写完成，旧用户的所有痕迹从 Git 对象数据库中彻底抹除。

---

# 第三阶段：HTTPS Token 推送失败——安全隐患浮出水面

## 症状：push 超时

准备强制推送到 GitHub 时，遇到了一个奇怪的问题：

```bash
$ git push --force origin main
# 超时，无任何响应
```

使用 `curl` 验证网络和认证：

```bash
# 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 密钥

```bash
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 连接时出现了意外结果：

```bash
$ ssh -T git@github.com
Hi YEI0907! You've successfully authenticated...
```

明明配的是新密钥，为什么认证成了另一个账号？因为 SSH 客户端默认会**逐个尝试 `~/.ssh/` 下的所有私钥**，而之前存在一个 `id_rsa` 私钥，它被优先匹配了。

## 解决方法：`~/.ssh/config` + `IdentitiesOnly`

```ssh-config
# 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` 而非 `~`：

```bash
# 错误写法
echo "..." > ~/.ssh/config     # ~ 在某些上下文中不被展开

# 正确写法
echo "..." > "$HOME/.ssh/config"
```

确认配置正确后：

```bash
$ ssh -T git@github.com
Hi new-user! You've successfully authenticated...
```

## 切换远程仓库 URL

HTTPS → SSH 的远程地址格式：

```bash
# 从 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
```

---

# 第五阶段：强制推送与最终清理

## 强制推送

```bash
git push --force origin main
```

⚠️ `--force` 是危险操作——它会用本地历史**覆盖**远程历史。如果有协作者基于旧的远程历史工作，他们的本地仓库会出现冲突。在工业环境中，更推荐 `--force-with-lease`：

```bash
# 更安全的强制推送：如果远程有本地不知道的新提交，拒绝推送
git push --force-with-lease origin main
```

## credential.helper 清理

最容易被忽略的一步——清除残留在 Git 配置中的凭证助手：

```bash
# 检查当前配置
$ git config --global credential.helper
store

# 这个 "store" 指向 ~/.git-credentials，即使切换到了 SSH，
# Token 仍然以明文形式躺在这个文件里

# 清除
git config --global --unset credential.helper
rm -f ~/.git-credentials
```

## 最终验证

```bash
# 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` 文件。任何能读取该文件的程序或用户都能获取完整的仓库操作权限。

```bash
# 这等于把钥匙放在门垫下
$ 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 权限：

```bash
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 密钥的安全加固清单

```bash
# ① 为私钥设置密码短语
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/config
```

## 5. 历史重写：`filter-branch` vs `git-filter-repo`

`git filter-branch` 虽然能用，但 Git 官方已经[不推荐](https://git-scm.com/docs/git-filter-branch#_warning)使用它：

| 维度 | filter-branch | git-filter-repo |
|:---|:---|:---|
| 速度 | 慢（逐 commit 遍历） | 快（批量操作） |
| 安全性 | 可能产生损坏的历史 | 严格验证 |
| 备份清理 | 需手动 `update-ref -d` | 自动清理 |
| 维护状态 | 基本冻结 | 活跃维护 |

现代替代方案：

```bash
# 安装
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 认证链路中的多个安全死角：

1. **明文 Token 存储** — `~/.git-credentials` 是最容易被忽略的泄露点
2. **历史提交的身份归属** — `git filter-branch` 能解决，但 `git-filter-repo` 更好
3. **SSH 密钥的选择和隔离** — ED25519 + `IdentitiesOnly` 是最佳实践
4. **清理远比配置重要** — 配置完 SSH 后删除 Token、清除 credential.helper 才是安全闭环

对于任何一个接手已有仓库的开发者来说，这套流程不仅是「改个配置」，而是一次完整的凭证安全检查——你永远不知道上一个维护者在系统里留下了哪些可以操作的入口。
