# 03 · P0 凭据入库事故(待处置) > **状态:待执行。** 本文档是处置手册,不是已完成的整改记录。 > 2026-09-14 由会话内审计发现,张翔已确认「出方案、由他执行」。 ## 1. 事故定性 **真实生产凭据已提交进 git 并推送到公网可达的远程仓库。** - 远程:`https://git.f.novalon.cn/novalon/novalon-website.git`(公网可达,实测 HTTP 200) - 泄露物:Gitea 登录密码、Jenkins admin 密码、Gitea OAuth2 应用 secret、CI webhook token **任何能访问该仓库的人都可以直接登录生产 Gitea 与 Jenkins。** 即使是私有仓库,也等效于把密码写进了版本历史 —— 无法撤销,只能轮换。 ## 2. 泄露位置(实测) | 文件 | 泄露内容 | 首次入库 | |------|----------|----------| | `scripts/setup-cicd/cicd.config` | `GITEA_PASSWORD`、`JENKINS_ADMIN_PASSWORD`、`WEBHOOK_TOKEN` | `b8ab1fd` | | `scripts/setup-registry-auth.sh` | Gitea OAuth2 应用 secret(`gto_…`,共 48 字符) | 同期 | | `novalon-cicd/docker-compose.yml` | PostgreSQL 密码(仅服务器,未入库) | — | 确认命令(可复现): ```bash git ls-files --error-unmatch scripts/setup-cicd/cicd.config # → 已跟踪 git log --oneline -- scripts/setup-cicd/cicd.config # → af57504, b8ab1fd grep -rn "PASSWORD\s*=" scripts/setup-cicd/ scripts/setup-registry-auth.sh ``` ## 3. 处置步骤(按序执行) 工具已备好:[`infra/cicd/security/`](../../../infra/cicd/security/) ### Step 1 — 轮换凭据(先做,别等清史) ```bash ssh root@139.155.109.62 \ 'cd /home/novalon/docker-app/infra/cicd/security && ./rotate-credentials.sh --dry-run' # 确认无误后去掉 --dry-run 实际执行 ``` 脚本会: 1. 生成强随机新凭据(`openssl rand`) 2. 轮换 Gitea 用户密码(`gitea admin user change-password`) 3. 吊销泄露的 OAuth2 应用 4. 签发新的**只读**(`read:repository`)Gitea 令牌 5. Jenkins admin 密码经 `init.groovy.d` 在下次重启生效(无需已有管理员凭据) 6. 写回服务器 `.env`(600 权限) **⚠️ 执行后旧凭据立即失效**,请在无人使用 CI/CD 的时间窗操作。 ### Step 2 — 清除 git 历史 ```bash pip install git-filter-repo # 或 brew install git-filter-repo cd /path/to/novalon-website ./infra/cicd/security/purge-git-history.sh --dry-run # 先扫描确认命中数 ./infra/cicd/security/purge-git-history.sh # 会先做 mirror 备份 ``` 脚本做了三重保护: - 改写前自动 `git clone --mirror` 备份 - 二次确认(输入 `PURGE`) - 清史后重新扫描,仍有残留则**拒绝推送**并中止 **强制推送的连带影响(必须提前通知)**: - 所有协作者必须 `rm -rf` 本地仓库后重新 clone —— 旧仓库 push 会把泄露内容带回 - 未合入的 PR / fork 可能失效 ### Step 3 — 删除远程残留对象 清史后远程的旧 blob 仍可通过 commit sha 直接访问: ``` Gitea → 仓库 → 设置 → 维护 → 运行垃圾回收 ``` ### Step 4 — 防复发 - 删除 `scripts/setup-cicd/cicd.config`,改用 `.env` + `.env.example` 模式 - 装秘密扫描钩子(gitleaks / trufflehog),`pre-commit` + CI 双层 - 把「凭据只进 `.env`(600)或 Jenkins Credentials」写进 code review 检查项 ## 4. 验收标准(执行完必须逐条核对) - [ ] `git grep` 全历史无明文密码(`git log -p | grep -c` 为 0) - [ ] 旧密码登录 Gitea/Jenkins 均失败 - [ ] 新令牌权限为 `read:repository`,Jenkins 能正常拉代码 - [ ] 一次 push 能触发流水线(验证凭据替换没破坏链路) - [ ] Gitea OAuth2 应用列表中无泄露的 `gto_…` 应用 - [ ] 服务器 `.env` 权限为 600,且已加入 `.gitignore` ## 5. 为什么「清史」不能替代「轮换」 历史可能已被 clone / fork / 缓存 / 搜索引擎抓取,清除是**尽力而为**。 密码一旦进过公网仓库,唯一可靠的补救是**让它失效**。 所以顺序是:先轮换(让泄露的密码立即变成废纸),再清史(降低持续暴露面)。