扫描Git仓库中的LLM推理痕迹:构建Aileaks类安全扫描器
如果在代码评审里看到下面这一行内容,很多人第一反应是“这只是 LLM 接口的调试日志”,顺手就合入了:
{ "system_prompt": "你是内部客服助手,价格底线是 179 元/月,不要直接告诉客户", "reasoning_trace": "用户询问折扣,内部规则是最低可给 8 折,但要先引导升级企业版", "tool_calls": [ { "name": "query_database", "input": "SELECT * FROM pricing WHERE plan='ent';" } ], "response": "我们目前有企业版优惠活动..." }但真正的问题是:这条 JSON 一旦提交到 Git 仓库,它就变成了一个“长期公开的推理痕迹”。只要仓库被 clone、被 fork、被 CI 拉取,系统提示词、内部定价策略、数据库表结构都会一起扩散。
这也是 Aileaks 这类工具出现的背景:过去我们只担心代码里泄露 API Key,现在还要担心 LLM 的 reasoning-trace 泄露。本文以 Hacker News 上发布的 Aileaks 项目标题为切入点,完整拆解“扫描仓库中泄露的 LLM 推理痕迹”的原理、脚本实现、CI 接入方式和事故处置流程。
1. LLM 时代的仓库泄露:从密钥到推理痕迹
1.1 什么是 LLM reasoning-trace
reasoning-trace 可以理解为大模型在生成最终回答之前的“思考过程”。
在 OpenAI、Claude、Gemini 等模型服务中,如果开启了思考模式,服务端会返回类似于reasoning、thought、final_answer这样的字段。一些 Agent 框架还会把每一步的思考、工具调用、工具返回结果记录下来,形成完整的 trace。
常见的字段包括:
| 字段 | 含义 | 泄露风险 |
|---|---|---|
| system_prompt | 系统提示词,包含人设、规则、知识边界 | 商业机密、内部规则泄露 |
| reasoning_trace | 模型的链式思考过程 | 暴露决策逻辑、知识库片段 |
| tool_calls | 模型调用的函数名和参数 | 暴露内部 API 结构、SQL 语句、数据库信息 |
| final_answer | 最终回答 | 可能包含未经脱敏的业务数据 |
| raw_request / raw_response | 完整请求响应体 | 可能包含用户身份、隐私数据 |
这些内容在开发阶段非常容易被写进日志文件、调试文件、测试数据,最后被随手 commit 进仓库。
1.2 常见泄露路径
从实际项目经验来看,LLM 推理痕迹进入仓库的路径大致有这几类:
.log文件:开发时打印的完整请求日志,包含 headers 和 body。traces目录:LLM 观测平台导出的 trace 文件,通常是 JSON 格式。- 测试数据:测试用例为了覆盖率,直接保存了真实的模型返回结果。
- AI 辅助开发产生的临时文件:IDE 插件、聊天工具自动保存的上下文文件。
dump.json、error.json:异常排查时导出的现场数据。
很多仓库出现泄漏,不是开发者故意提交密钥,而是日志体系设计不合理,把敏感内容当成普通文本输出了。
1.3 为什么传统 Secret Scanning 不够用
传统的密钥扫描工具,比如 gitleaks、trufflehog,擅长识别固定模式的密钥,比如:
sk-xxxxxxxxxxxxxxxxxxxxxxxx AKIAXXXXXXXXXXXXXXXX ghp_xxxxxxxxxxxxxxxx但 LLM reasoning-trace 的泄露往往不是标准密钥,而是自然语言。例如:
你是内部客服,价格底线是 179 元/月,不要告诉用户这段文字没有密钥模式,正则表达式很难命中。而且很多敏感片段会被 JSON 转义、Base64 编码、压缩打包,传统工具只能看到一串无意义的字符。
所以我们需要一套面向 LLM 场景的检测思路:
- 识别密钥特征;
- 识别 reasoning 相关字段名;
- 识别系统提示词和内部规则的关键词;
- 识别高熵字符串;
- 结合 Git 历史扫描,哪怕是已经删除的文件也能被找回来。
2. Aileaks 这类工具的核心思路
2.1 工具定位
从名称和发布信息来看,Aileaks 做的事情可以概括为:scan repos for leaked LLM reasoning-trace secrets,也就是扫描代码仓库中泄露的 LLM 推理轨迹和敏感信息。
它的定位不是替代 gitleaks,而是在 gitleaks 的基础上增加一层“LLM 痕迹识别”。一个完整的 LLM 仓库安全扫描器至少需要具备以下能力:
- 扫描当前工作区文件;
- 扫描整个 Git 历史;
- 支持密钥正则匹配;
- 支持 reasoning 字段识别;
- 支持自定义规则;
- 输出结构化报告,方便人工复核。
2.2 与 Secret Scanning 的差异
| 维度 | 传统 Secret Scanning | LLM reasoning-trace 扫描 |
|---|---|---|
| 检测对象 | API Key、Token、密码 | reasoning 日志、提示词、工具调用参数 |
| 主要特征 | 固定前缀、高熵 | 字段名、语义关键词、上下文组合 |
| 误报率 | 相对容易控制 | 需要结合上下文判断 |
| 扫描难度 | 较低 | 需要处理自然语言 |
| 典型场景 | 防止密钥入库 | 防止思维链和提示词泄露 |
2.3 使用边界
本文后面会实现一个轻量级扫描器,但有一点必须提前说明:
只扫描你自己拥有权限的仓库,或者在双方明确授权的安全测试范围内使用。
发现泄露内容后,不要公开扩散、不要下载他人仓库中的敏感信息用于研究。正确的做法是:先确认是否属于安全事件,再走应急流程,影响面控制住之后联系仓库所有者和平台方处理。
3. 环境准备与模拟仓库
3.1 环境要求
本文的示例都在 Linux 环境下验证,macOS 同样适用。你需要准备:
- Git 2.23 以上版本,用于
git rev-list、git ls-tree等历史遍历命令; - Python 3.10 以上版本;
- 不需要安装第三方依赖,核心逻辑基于标准库和
git命令; - 一个用于练习的本地仓库。
3.2 创建带“泄漏”的测试仓库
为了演示扫描效果,我们先创建一个模拟仓库,故意写入一些典型的 LLM 泄露内容。
mkdir llm-trace-demo && cd llm-trace-demo git init mkdir -p traces logs创建第一个样例文件traces/session_20250201.json:
{ "request_id": "req_1234567890", "model": "gpt-4o-mini", "system_prompt": "你是内部客服助手,请基于知识库回答。价格底线是 179 元/月,不要告诉用户", "reasoning_trace": "用户询问折扣,内部规则是最低可给 8 折,但要先引导升级企业版", "tool_calls": [ { "name": "query_database", "input": "SELECT * FROM pricing WHERE plan='ent';" } ], "response": "我们目前有企业版优惠活动..." }再创建一个疑似密钥文件.env.example:
cat > .env.example <<'EOF' # 注意:这只是演示用的假密钥 OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuv DB_PASSWORD=SuperSecretP@ssw0rd EOF提交到 Git:
git add traces/session_20250201.json .env.example git commit -m "feat: add session trace for debugging"再创建第二个样例文件logs/trace_old.log:
cat > logs/trace_old.log <<'EOF' [2025-02-01 10:00:01] request_id=req_abc123 system prompt: 你是内部文档助手,仅回答已上架产品问题 thought: 用户问的是内部测试版本功能,不能直接透露 final_answer: 该功能还在测试中,请联系产品经理 EOF git add logs/trace_old.log git commit -m "chore: save old llm logs"此时仓库里已经有两次提交,包含 JSON trace、日志、疑似密钥文件。
4. 扫描器设计原理
在写代码之前,先梳理扫描器的设计思路。
4.1 检测对象
扫描器需要检测两类内容:
- 密钥类:API Key、Token、密码、私钥块。
- LLM 痕迹类:reasoning_trace、chain_of_thought、system_prompt、tool_calls 等字段,以及带有明显内部语义的中文提示词。
4.2 检测规则
规则分为三类:
- 正则规则:匹配固定模式的密钥和字段名。
- 关键词规则:匹配“不要告诉用户”“内部规则”“系统提示词”等语义特征。
- 熵检测:计算字符串的信息熵,高熵字符串大概率是密钥、Token 或编码后的敏感数据。
熵检测的原理很简单:正常英文单词和中文文本的字符分布有一定规律,而随机密钥的字符分布几乎是均匀的,信息熵较高。当一段连续字符串的熵大于阈值时,就值得人工关注。
4.3 当前工作区扫描与历史扫描
当前工作区扫描只看git ls-files列出的被追踪文件,优点是快,适合接入 pre-commit。
历史扫描则需要遍历所有 commit:
git rev-list --all -> 拿到所有 commit hash git ls-tree -r --name-only <commit> -> 拿到该 commit 下的所有文件 git show <commit>:<file> -> 拿到某个历史版本的文件内容这样即使是已经删除的文件,也能在历史中被扫描出来。
5. 实战:实现一个 reasoning-trace 扫描器
5.1 项目结构
llm-trace-demo/ ├── traces/ │ └── session_20250201.json ├── logs/ │ └── trace_old.log ├── .env.example └── scripts/ └── scan_repo.py5.2 扫描脚本完整代码
创建scripts/scan_repo.py:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 轻量级 LLM reasoning-trace 仓库扫描器 功能: 1. 扫描 Git 历史中的文件内容 2. 匹配密钥正则、reasoning 字段、敏感关键词 3. 使用熵检测辅助识别高熵字符串 4. 输出结构化命中报告 """ import argparse import math import re import subprocess import sys from pathlib import Path # 密钥规则 SECRET_PATTERNS = [ (r"openai_api_key", r"\bsk-[A-Za-z0-9_\-]{20,}\b"), (r"aws_access_key", r"\bAKIA[0-9A-Z]{16}\b"), (r"private_key_block", r"-----BEGIN (?:RSA|EC|OPENSSH) PRIVATE KEY-----"), (r"generic_api_secret", r"(?i)\b(?:api[_-]?key|secret|token|password)\b[\"'\s:=]+[\"']?[A-Za-z0-9_\-/+=]{16,}"), ] # LLM reasoning 相关规则 REASONING_PATTERNS = [ (r"reasoning_trace_key", r"\"reasoning(_| )?trace\"\s*:"), (r"chain_of_thought", r"(?i)\bchain[_-]?of[_-]?thought\b"), (r"final_answer_key", r"\"final[_ ]answer\"\s*:"), (r"system_prompt_snippet", r"(?i)(system\s+prompt|你是内部|内部规则|不要告诉用户|指令词)"), (r"tool_call_key", r"\"tool[_ ]?calls?\"\s*:"), (r"thought_observation", r"(?i)\b(thought|observation)\s*:"), ] # 跳过二进制、依赖锁文件、压缩包 SKIP_SUFFIXES = { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".woff", ".woff2", ".pdf", ".zip", ".gz", ".jar", ".class", ".so", ".dll", ".exe", ".pyc", ".pyo", ".min.js", ".map", ".lock", } HIGH_ENTROPY_THRESHOLD = 3.5 MAX_MATCH_CHARS = 160 MAX_FILE_SIZE = 5 * 1024 * 1024 def shannon_entropy(text: str) -> float: """计算字符串的信息熵,熵越高,越像随机密钥。""" if not text: return 0.0 length = len(text) freq = {} for ch in text: freq[ch] = freq.get(ch, 0) + 1 entropy = 0.0 for count in freq.values(): p = count / length entropy -= p * math.log2(p) return entropy def should_skip(path: str) -> bool: return any(path.endswith(suffix) for suffix in SKIP_SUFFIXES) def scan_text(text: str, location: str, hits: list): """在单段文本中执行规则匹配。""" for rule, pattern in SECRET_PATTERNS + REASONING_PATTERNS: try: matches = re.finditer(pattern, text, flags=re.IGNORECASE) except re.error as e: print(f"[rule error] {rule}: {e}", file=sys.stderr) continue for m in matches: start = max(0, m.start() - 40) end = min(len(text), m.end() + 120) snippet = text[start:end].replace("\n", " ").replace("\r", " ") hits.append({ "location": location, "rule": rule, "snippet": snippet[:MAX_MATCH_CHARS], }) # 高熵字符串检测 for token in re.findall(r"[A-Za-z0-9_\-/+=]{20,}", text): if shannon_entropy(token) >= HIGH_ENTROPY_THRESHOLD: hits.append({ "location": location, "rule": "high_entropy_token", "snippet": token[:80], }) def scan_working_tree(repo: Path): """扫描当前工作区中被 Git 追踪的文件。""" hits = [] try: out = subprocess.check_output( ["git", "ls-files"], cwd=repo, text=True, errors="ignore" ) except subprocess.CalledProcessError as e: print(f"git ls-files 失败: {e}", file=sys.stderr) return hits for rel in out.strip().splitlines(): if not rel: continue if should_skip(rel): continue path = repo / rel if not path.exists(): continue try: data = path.read_text(encoding="utf-8", errors="ignore") except Exception as e: print(f"[skip] {path}: {e}", file=sys.stderr) continue scan_text(data, f"working_tree:{rel}", hits) return hits def scan_history(repo: Path): """扫描所有历史提交中的文件内容。""" hits = [] try: commits = subprocess.check_output( ["git", "rev-list", "--all"], cwd=repo, text=True, errors="ignore" ).strip().splitlines() except subprocess.CalledProcessError as e: print(f"git rev-list 失败: {e}", file=sys.stderr) return hits for commit in commits: try: files = subprocess.check_output( ["git", "ls-tree", "-r", "--name-only", commit], cwd=repo, text=True, errors="ignore" ).strip().splitlines() except subprocess.CalledProcessError: continue for rel in files: if not rel or should_skip(rel): continue try: data = subprocess.check_output( ["git", "show", f"{commit}:{rel}"], cwd=repo, text=True, errors="ignore", stderr=subprocess.DEVNULL ) except subprocess.CalledProcessError: # 可能是子模块或 LFS 文件,跳过 continue if len(data) > MAX_FILE_SIZE: continue scan_text(data, f"{commit}:{rel}", hits) return hits def deduplicate(hits): """按 location + rule + snippet 去重。""" seen = set() result = [] for hit in hits: key = (hit["location"], hit["rule"], hit["snippet"]) if key not in seen: seen.add(key) result.append(hit) return result def print_report(hits): if not hits: print("未发现疑似泄露内容。") return print(f"发现 {len(hits)} 处疑似泄露:\n") print(f"{'序号':<4} {'位置':<50} {'规则':<25} {'命中片段'}") print("-" * 120) for idx, hit in enumerate(hits, 1): print(f"{idx:<4} {hit['location'][:48]:<50} {hit['rule']:<25} {hit['snippet'][:40]}") def main(): parser = argparse.ArgumentParser(description="Scan LLM reasoning-trace secrets") parser.add_argument("--repo", type=str, default=".", help="仓库路径") parser.add_argument("--history", action="store_true", help="是否扫描 Git 历史") args = parser.parse_args() repo = Path(args.repo).resolve() if not (repo / ".git").exists() and not (repo / "HEAD").exists(): print("当前目录不是 Git 仓库。", file=sys.stderr) sys.exit(1) hits = [] print(f"[*] 扫描仓库: {repo}") print("[*] 正在扫描当前工作区...") hits.extend(scan_working_tree(repo)) if args.history: print("[*] 正在扫描 Git 历史(可能较慢)...") hits.extend(scan_history(repo)) hits = deduplicate(hits) print_report(hits) if __name__ == "__main__": main()这个脚本自包含,没有第三方依赖,核心思路如下:
scan_working_tree只扫描当前追踪文件,速度快,适合日常检查。scan_history通过git rev-list --all拿到所有提交,再逐个提取文件内容,适合做全量历史检查。scan_text同时做正则匹配和熵检测,命中结果统一保存。deduplicate避免同一内容在多个版本里重复出现,让报告更清晰。
5.3 运行扫描器
先扫描当前工作区:
cd llm-trace-demo python3 scripts/scan_repo.py --repo .预期输出大致如下:
[*] 扫描仓库: /home/user/llm-trace-demo [*] 正在扫描当前工作区... 发现 5 处疑似泄露: 序号 位置 规则 命中片段 1 working_tree:traces/session_20250201.json system_prompt_snippet 你是内部客服助手,请基于知识库回答。价格底线是 179 元/月,不要告诉用户 2 working_tree:traces/session_20250201.json reasoning_trace_key "reasoning_trace": "用户询问折扣,内部规则是最低可给 8 折... 3 working_tree:traces/session_20250201.json tool_call_key "tool_calls": [ { "name": "query_database", "input": "SELECT * FROM... 4 working_tree:.env.example openai_api_key sk-1234567890abcdefghijklmnopqrstuv 5 working_tree:.env.example generic_api_secret DB_PASSWORD=SuperSecretP@w0rd再扫描历史:
python3 scripts/scan_repo.py --repo . --history由于之前已经记录了两次提交,历史扫描会额外扫到logs/trace_old.log中的 thought 和 final_answer 内容。
5.4 为什么需要 history 扫描
只扫描当前工作区会漏掉一类重要情况:开发者发现文件敏感后,用git rm删除文件并提交,但历史中仍然保留了旧版本。只要仓库没有被重写,任何人都能通过git log、git show找回被删除的内容。
这也是 GitHub 上一些“已删除密钥”仍然被爬虫扫描到的原因。所以真正有效的仓库敏感信息排查,必须把历史扫描纳入检查范围。
6. 把扫描接入 pre-commit 和 CI
6.1 pre-commit 本地钩子
本地开发阶段,最好在提交前就拦截敏感信息。使用 pre-commit 框架时,可以在.pre-commit-config.yaml中配置一个本地钩子:
repos: - repo: local hooks: - id: scan-reasoning-trace name: scan-reasoning-trace entry: python3 scripts/scan_repo.py --repo . language: system pass_filenames: false verbose: truepass_filenames: false表示不把文件名逐个传进去,而是由脚本自己扫描整个仓库。
这样每次git commit之前,pre-commit 会自动运行扫描。如果发现命中,提交会被中断,开发者需要先处理敏感内容。
注意:在本地钩子中扫描整个历史会导致提交变慢。建议 pre-commit 阶段只扫描当前工作区,历史全量扫描放到 CI 或定时任务中执行。
6.2 GitHub Actions 定时全量扫描
CI 环境中,可以配置一个工作流做全量历史扫描:
name: llm-secret-scan on: push: branches: [ "main" ] pull_request: schedule: - cron: "0 2 * * 1" jobs: scan: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run reasoning-trace scanner run: python3 scripts/scan_repo.py --repo . --history这里有两个关键点:
fetch-depth: 0表示克隆完整历史。如果不加这个参数,GitHub Actions 默认只拉取最新一次提交,历史扫描会没有数据。schedule用于每周自动执行一次全量扫描,弥补 push 阶段扫描可能漏掉的情况。
实际团队落地时,还可以把扫描脚本做成独立 Docker 镜像,或者接入内部的代码安全平台。
7. 发现泄露后的处置流程
扫描器发现命中只是开始,真正的难点在于处置。
7.1 先判断是否为真实命中
很多命中是误报:
- 示例代码中的假密钥;
- 测试用例里故意构造的字段;
- 文档中引用了公开的密钥正则;
- 数据样本来自公开数据集。
所以第一步永远是人工复核,确认命中内容是否真的包含:
- 可用的密钥;
- 未公开的系统提示词;
- 内部业务规则;
- 用户隐私数据;
- 数据库连接信息。
不要一看到命中就直接改写历史,那样会造成不必要的团队协作成本。
7.2 立即撤销密钥
如果命中内容包含真实的 API Key、Token、数据库密码,第一步不是删 Git 历史,而是马上让密钥失效:
- OpenAI/云服务厂商的 Key 直接在控制台 revoke;
- 数据库密码立即修改;
- 内部 API Token 重新生成。
原因是:仓库一旦被推送过远程,任何有权访问该仓库的人员都可能已经拿到密钥。单纯删除仓库历史并不能保证密钥没有被复制。
密钥撤销后,再考虑清理历史。
7.3 使用 git filter-repo 清理历史
清理历史时,推荐使用git-filter-repo,它比 filter-branch 更快、更安全。
安装方式:
pip install git-filter-repo移除单个文件:
git filter-repo --path traces/session_20250201.json --invert-paths移除包含特定文本的提交:
git filter-repo --replace-text <(echo "sk-1234567890abcdefghijklmnopqrstuv==>xxx")运行前必须:
- 备份整个仓库;
- 通知所有合作者,历史重写后需要重新 clone;
- 确认远程端的保护规则,必要时先关闭 force push 保护;
- 清理本地的 reflog 和垃圾对象。
rm -rf .git/refs/original/ git reflog expire --expire=now --all git gc --prune=now --aggressive7.4 平台缓存的处理
如果仓库托管在 GitHub、GitLab 等平台,历史重写后,平台侧可能还有:
- fork 副本;
- CI 缓存;
- 平台安全扫描器的缓存;
- 已生成的工单、评论中的链接。
这些内容不一定能通过本地git push --force清除。必要时应联系平台支持团队,或者由平台安全响应流程介入。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 扫描结果大量误报 | 规则太宽,比如通用 token 正则匹配到了示例代码 | 增加白名单、提高熵阈值、要求同时命中字段名和值 |
| 明明文件还在,但扫描不到 | 文件被 .gitignore 忽略,不在git ls-files输出中 | 检查.gitignore;如果文件本身未入库,风险相对较低 |
| history 扫描结果为空 | 本地 clone 用了--depth=1,没有完整历史 | 执行git fetch --unshallow后再扫描 |
| git show 报错跳过文件 | 文件可能是子模块或 Git LFS 指针 | 在错误日志中单独记录 LFS 文件,使用 LFS 工具检查 |
| 扫描太慢 | 每个 commit 的每个文件都调用一次 git show | 使用git log -p流式扫描,或建立文件指纹缓存 |
| pre-commit 误拦截正常提交 | 测试数据中包含 demo key | 在规则中忽略test/、fixtures/目录,或使用短标记{{SECRET}} |
| 二进制文件里的敏感信息扫不到 | 脚本默认跳过二进制文件 | 增加strings提取流程,单独扫描二进制制品 |
| 删除文件后重新扫描仍然命中 | 历史记录中仍保留旧 blob | 历史重写后执行 gc 和 reflog 清理 |
这里需要重点说明的是:扫描工具的意义是发现问题,而不是证明“没有风险”。一次全量扫描没有命中,不代表仓库绝对安全,只能说明当前规则覆盖范围内的内容没有被检测到。随着 LLM 应用形态变化,检测规则也需要持续更新。
9. 最佳实践与工程建议
9.1 从日志源头脱敏
很多泄露问题在代码提交前就已经发生了。最有效的手段不是扫描,而是让敏感内容不进入日志。
建议在 LLM 网关层或日志输出层做统一脱敏:
- 请求体中的 system_prompt 默认只记录 hash;
- reasoning_trace 在生产环境默认关闭;
- tool_calls 参数中的 SQL、文件路径、用户 ID 做掩码;
- 日志输出 PII 字段统一替换为占位符。
def mask_sensitive_fields(data: dict) -> dict: mask_keys = {"system_prompt", "reasoning_trace", "api_key", "password", "token"} for key, value in data.items(): if key in mask_keys and isinstance(value, str): data[key] = value[:4] + "****" + value[-4:] return data这个方案比扫描更靠前,成本也最低。
9.2 分层扫描策略
实际工程中,建议把扫描分成三个层级:
- 开发阶段:pre-commit 扫描当前工作区,快速反馈;
- 合并阶段:CI 扫描新增提交内容,结合 gitleaks 类工具;
- 周期阶段:每周全量扫描 Git 历史,排查长期积累的泄露。
三个层级使用同一套规则库,但执行频率不同,避免每次提交都被历史扫描拖慢。
9.3 规则库要持续迭代
LLM 的迭代速度很快,新的字段名、新的日志格式不断出现。建议把规则抽成独立的 YAML 或 JSON 文件,而不是写死在代码里。
rules: - id: reasoning_trace_key pattern: '"reasoning(_| )?trace"\s*:' category: llm_trace - id: system_prompt_keyword pattern: '(?i)(system\s+prompt|你是内部|不要告诉用户)' category: prompt_leak这样当 OpenAI 发布新字段、新 Agent 框架引入新的 trace 格式时,可以直接通过配置更新,不需要改动扫描主程序。
9.4 权限和敏感数据隔离
除了扫描,还要从权限上限制敏感内容的传播范围:
- 训练数据、评测集、LLM trace 文件不要放在应用仓库中;
- 提示词模板如果属于商业机密,单独建私有仓库管理;
- 仓库访问权限遵循最小权限原则;
- 为外部协作者配置只读或受限访问。
9.5 关注 LLM 应用的供应链安全
当前 LLM 应用开发中,大家更关注模型精度(fp16/fp32/bf16 的选择)、Agent 编排框架、RAG 知识库效果,但供应链安全同样不能忽视。
reasoning-trace 泄露只是其中一个入口。如果 Agent 框架的配置文件中包含了内部工具的 API 地址、服务账号信息,同样会被扫描工具识别为高价值目标。建议在做模型选型和框架选型时,把“日志脱敏能力”“trace 导出权限控制”纳入评估维度。
10. 总结与进一步学习
本文围绕 Aileaks 的发布话题,把“扫描仓库中泄露的 LLM reasoning-trace secrets”这条链路完整拆解了一遍。
你可以从以下几个方面继续深入:
- 先跑通本文的扫描脚本,在测试仓库里观察命中结果;
- 阅读 gitleaks 的规则库设计,把其中针对密钥的正则合并进来;
- 学习 git-filter-repo 的使用方式,在测试环境中模拟一次历史重写;
- 结合公司的日志规范,把系统提示词和推理过程的脱敏能力前置到日志输出层。
有一点需要特别提醒:扫描工具可以帮你发现历史遗留问题,但真正解决问题的关键,是让开发团队意识到“LLM 推理过程本身可能就是敏感数据”。把扫描加入日常开发流程,比事后清理更省成本。安全建设不需要一开始就做得很重,先从一条最简单的规则开始,把误报率调到可接受范围,再逐步扩大覆盖范围即可。
