凭据一旦提交到 Git,就成为版本化对象关系的一部分,协作者和自动化系统都有可能复制它。从最新目录树删除文件,并不会删除先前包含它的提交。实际应建立的边界,是让凭据值不进入源码历史,再通过获得授权的运行时机制供应。仓库配置可以说明怎样取得秘密,而不需要把秘密本身写进可复制的源文件。
坏掉的样子 一个看起来干净的检出,可能掩盖历史版本中已经暴露的值。新克隆通常会收到可达历史,包括已删除文件的旧版本。不能说 Git 在任何清理策略下都会永远保留所有对象,但“删掉今天的文件”确实不等于清理历史。即使重写一个仓库,也无法召回已经独立分发出去的克隆、分叉、缓存和构建产物,清理某一位置不能代表所有副本都已消失。
面对真实泄漏,应先撤销或替换相关凭据,并确认哪些系统会接受它。删除一行改变的是发布内容,不是认证状态。历史重写应单独协调,因为它会改变提交标识,也可能打断协作者的工作。说明事件时,不能再把原值复制到问题单、提交说明、扫描报告或事故笔记里,否则处理过程本身又制造了新的传播位置。
运行时注入也不会自动保密。环境值可能被子进程继承或被诊断工具采集;文件可能被具有足够访问权限的身份读取。应限制消费服务的权限,控制交付通道,避免把真实值写入日志和生成结果。即使文件位于仓库外,也仍然需要明确的拥有者与生命周期,不能把“没有提交到 Git”当作全部秘密管理工作已经完成。
快速验证 在安装了 Git 的 POSIX 系统上运行下列 Python 脚本。它使用明确的虚构标记,绝不使用任何有效凭据;两个仓库和运行时文件都位于同一个临时目录,退出后一起清理。夹具排除继承的 GIT_* 覆盖值、系统及全局配置和模板钩子,记录中的 Git 版本为 2.50.1:
from pathlib import Path from tempfile import TemporaryDirectory import io,os,stat,subprocess,sys,tarfile env={key:value for key,value in os.environ.items() if not key.startswith('GIT_')} env.update(GIT_CONFIG_NOSYSTEM='1',GIT_CONFIG_GLOBAL=os.devnull) with TemporaryDirectory(prefix='credential-repo-lab-') as directory: base=Path(directory);repo=base/'source';repo.mkdir() def git(*args,cwd=repo,binary=False): result=subprocess.run(['git',*args],cwd=cwd,env=env,capture_output=True,text=not binary) assert result.returncode==0,result.stderr return result.stdout git('init','-q','--template=');git('config','user.name','Credential Lab') git('config','user.email','credentials@example.invalid') git('config','commit.gpgsign','false') dummy='EXAMPLE-ONLY-NOT-A-REAL-SECRET\n' (repo/'fixture.env').write_text(dummy) (repo/'app.txt').write_text('read the configured credential file at runtime\n') git('add','--','fixture.env','app.txt');git('commit','-qm','Synthetic exposure fixture') original=git('rev-parse','HEAD').strip() git('rm','--','fixture.env');git('commit','-qm','Remove fixture from current tree') assert not (repo/'fixture.env').exists() assert git('show',original+':fixture.env')==dummy clone=base/'clone';git('clone','-q','--no-local',str(repo),str(clone)) assert not (clone/'fixture.env').exists() assert git('show',original+':fixture.env',cwd=clone)==dummy print('current file absent; historical fixture readable in source and clone') archive=git('archive','--format=tar','HEAD',binary=True) with tarfile.open(fileobj=io.BytesIO(archive),mode='r:') as bundle: assert bundle.getnames()==['app.txt'] print('current-tree archive excludes fixture; Git history still contains it') runtime=base/'runtime';runtime.mkdir(mode=0o700) secret_file=runtime/'credential';secret_file.touch(mode=0o600) secret_file.write_text(dummy) assert stat.S_IMODE(secret_file.stat().st_mode)==0o600 child_env=dict(env,CREDENTIAL_FILE=str(secret_file)) child="from pathlib import Path; import os; p=Path(os.environ['CREDENTIAL_FILE']); assert p.read_text(); print('credential loaded without printing its value')" result=subprocess.run([sys.executable,'-c',child],cwd=clone,env=child_env,text=True,capture_output=True) assert result.returncode==0,result.stderr assert dummy.strip() not in result.stdout+result.stderr print(result.stdout.strip()) 第一项确认,两个当前工作区都没有样本文件,但都能通过早先的提交读取它的原始内容。克隆使用普通 Git 传输路径,关闭本地硬链接优化,因此验证的是另一个实际建立起来的对象数据库,不只是两个路径恰巧指向同一份文件。脚本在内部比较虚构标记,结果只说明是否保留,没有打印它的值,保留了检查与泄漏输出之间的区别。
...