W weiserv
← 返回博客

把生产密码写进了 Git:一次提交前拦截与完整修复

把生产密码写进了 Git:一次提交前拦截与完整修复

事情是这样的:我在整合一个项目的时候,顺手写了个同步生产库的脚本。为了让脚本能跑,我直接把数据库密码写在了里面:

DB_PASSWORD = "Pr0d-Db-P@ssw0rd-2026x****"   # 生产库密码,就这么硬编码了

然后我在工作日志里记录当天做了什么,也很自然地写上了"库名 / 用户名 / 密码"。

接着 git add -Agit commit

提交完的那一刻我意识到不对劲。 立刻查了一下——密码已经进了版本库。

万幸的是:我还没 push。所以这件事最终只是一次虚惊,而不是一次事故。但复盘下来,里面的教训很值得写下来。


一、为什么"还没 push"是决定性的

很多人有个误解:提交(Commit)就等于泄露了。其实不是。

阶段 影响范围 能否挽回
工作区改了 仅本机 能(改回来就行)
已 commit 仅本地仓库 --amend / 重写历史)
已 push 所有人可见(含历史) 基本不能——改历史也没用,别人可能已经拉走了,必须轮换凭据

所以这次能全身而退,唯一原因就是提交后、推送前的那次自查

真正该做的不是"指望自己记得检查",而是把检查变成流程的一部分(见第四节)。


二、三个泄露点(我全踩了)

复盘发现,泄露不止一处:

# 泄露点 为什么容易中招
1 脚本里硬编码密码 写脚本时只想"让它先跑起来",随手就把密码填进去了
2 工作日志里明文记录 记录时追求"信息完整",把连接信息整段贴了上去
3 文档/报告里附带 之前写的对齐报告里也带了完整连接串

共同特点:都不是故意的,都是"顺手"。


三、四步修复(commit 之后、push 之前)

步骤 1:定位——先搞清楚泄露了哪些文件

不要凭印象,直接扫:

# 扫已跟踪文件
git ls-files | xargs grep -l "Pr0d-Db-P@ssw0rd-2026x" 2>/dev/null

# 顺手把其它敏感特征也扫一遍
git ls-files | xargs grep -lE "BEGIN OPENSSH|\.pem|AKIA[0-9A-Z]{16}" 2>/dev/null

我的结果:命中 2 个文件(一个脚本、一份日志)。

步骤 2:治本——脚本不再持有密码

把"读密码"改成按优先级取值,并给出清晰的失败提示:

def _get_password():
    # 1) 环境变量
    pw = os.environ.get("PROD_DB_PASSWORD", "").strip()
    if pw:
        return pw
    # 2) 已被 .gitignore 排除的本地文件
    secret_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "..", "secrets", "db_password")
    if os.path.exists(secret_file):
        with open(secret_file, encoding="utf-8") as f:
            pw = f.read().strip()
        if pw:
            return pw
    return ""

DB_PASSWORD = _get_password()

拿不到就明确报错,而不是悄悄用一个空密码去连。

步骤 3:脱敏——文档里的明文替换成"指针"

不要直接删掉(那样以后自己也不知道去哪找),替换成指向真正存放位置的说明

sed -i 's/Pr0d-Db-P@ssw0rd-2026x[A-Za-z0-9]*/<生产DB密码:见 <部署配置文件> \/ ops\/secrets\/db_password(不入库)>/g' 相关文件.md

步骤 4:改历史 + 补 .gitignore

因为还没 push,可以安全地重写提交:

# 把含敏感内容的目录从索引移除(文件仍在磁盘)
git rm -r --cached .workbuddy/memory -q
git add -A
git commit --amend -m "chore: ...(修订说明中写明已移除凭据)"

同时在 .gitignore 里补上该排除的东西:

# 凭据
*.pem
*.key
ops/secrets/
.env

# 含生产数据的导出
*.sql

# 会话/记忆日志(极易误记凭据,建议直接排除)
.workbuddy/memory/

最后复查一遍

git ls-files | xargs grep -lE "Pr0d-Db-P@ssw0rd-2026x|BEGIN OPENSSH" 2>/dev/null
# 无输出 = 干净

四、顺带发现:.gitignore 漏掉的东西比想象中多

这次自查还顺带扫出三个"体积炸弹",都是原本 .gitignore 没挡住的:

漏网之鱼 体积 说明
WSL2 安装介质(.AppxBundle / .msi / rootfs.tar.gz 2.1 GB 我特意保留的离线安装包
某个 demo 用的完整 Python 虚拟环境 约 14 MB(含大量 .exe 里面连 python.exe 都有
数据库导出文件 不仅大,还含生产真实数据 双重风险

补上规则后,整个仓库从约 2.2 GB 降到 4.32 MB

经验:.gitignore 要按"类别"排除,不要按"文件名"排除
*.msi*.exe*.sql*.tar.gz,比写具体文件名可靠得多。


五、把它变成流程:提交前自检脚本

靠"记得检查"一定会漏。把这个挂进 pre-commit(或者至少养成本地跑一下的习惯):

#!/usr/bin/env bash
# pre-commit-credential-check
set -euo pipefail

PATTERNS='BEGIN OPENSSH|BEGIN RSA PRIVATE|BEGIN OPENSSH PRIVATE KEY|AKIA[0-9A-Z]{16}|wx[0-9a-f]{16}|db_password *[:=] *[^ <]'

found=0
while IFS= read -r f; do
  if grep -qE "$PATTERNS" "$f" 2>/dev/null; then
    echo "🔴 疑似凭据: $f"
    found=1
  fi
done < <(git diff --cached --name-only --diff-filter=ACM)

# 体积检查:单个暂存文件 > 5MB 就提醒
while IFS= read -r f; do
  size=$(stat -c%s "$f" 2>/dev/null || echo 0)
  if [ "$size" -gt 5242880 ]; then
    echo "🟡 大文件(>5MB): $f ($(( size / 1024 / 1024 ))MB)"
  fi
done < <(git diff --cached --name-only --diff-filter=ACM)

if [ "$found" -eq 1 ]; then
  echo "❌ 提交中止:请先移除凭据"
  exit 1
fi
echo "✅ 凭据自检通过"

提示:在项目自己的"业务密码"这类非通用模式上,上面的正则拦不住。
更稳妥的做法是维护一份本项目专属的敏感串清单,一起扫。


六、如果已经 push 了怎么办

这次没走到这一步,但结论很明确:

  1. 立刻轮换(Rotate)泄露的凭据——改数据库密码、吊销并重新签发 API Token/密钥。这是唯一真正有效的动作。
  2. 重写历史(git filter-repo / BFG)只是补充,不能替代轮换;
  3. 检查该凭据是否还出现在别处(CI 配置、部署脚本、备份、聊天记录);
  4. 事后补上自检流程,避免重演。

顺序不能反:先轮换,再清理历史。很多人第一反应是去改 Git 历史,那期间凭据仍然有效,风险窗口反而被拉长了。


小结

认知 说明
commit ≠ 泄露,push 才是分水岭 提交后仍有挽回余地,--amend 即可
泄露往往是"顺手"造成的 脚本、日志、报告是三个高发区
排查要扫全,不能凭印象 git ls-files \| xargs grep 一次搞定
.gitignore 按类别排除 别按文件名,否则必漏
检查要固化成流程 靠记忆一定会漏,挂进 pre-commit
真 push 了 → 先轮换 改历史只是补充,不能替代轮换

整套修复花了不到二十分钟。但它提醒我一件事:安全习惯的价值不在于"小心",而在于把"小心"变成不需要思考的自动动作。


本文环境:Windows 11 + Git / WSL2 Ubuntu-22.04 / Python 3.13
文中案例为真实事件复盘,凭据均已脱敏;涉及密钥管理的官方规范请以所用平台文档为准。

广告位占位 · post-inline