38 KiB
密钥出库与轮换 (批次0) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 把已泄露到 git 的生产密钥(OSS/SMS/OpenAI/Dify/PUSH_URL/MiniMax)从仓库「出库」:删掉跟踪、补 .gitignore、.env.example 仅留占位符;同时明确告知云端密钥轮换与 git 历史清理为运维动作,AI 不可执行(也无法执行——AI 没有云控制台/团队强推权限)。
Architecture:
- 代码侧(AI 可做):
backend/deploy/envs/*.env+docker/.env*+ 根.gitignore之外的.env加进.gitignore;git rm --cached删跟踪;backend/.env.example真实密钥替换为<REPLACE_ME>占位符;创建docker/.gitignore(当前不存在)。 - 运维侧(AI 不可做):
- 云端轮换:阿里云 RAM 子账号 AccessKey、OpenAI/微达 中转站 API Key、Dify App API Key、uniPush URL/TOKEN、SMS AccessKey——在云控制台禁用旧 key、签发新 key、更新所有部署消费方。
- git 历史清理:
git filter-repo --invert-paths删历史记录,强推 + 全员rm -rf && git clone。
- 部署消费方(AI 可做模板):更新
docker-compose.{local,prod}.yml、k8s/helm/topfans/values-prod.example.yaml、backend/dev.sh的密钥注入方式(env_file 指向仓库外、或 helm secret 模板从外部 Secret 引用)。
Tech Stack: Git(git rm --cached、git filter-repo)、阿里云 RAM/OpenAI/Dify 控制台(运维)、K8s Secret/External Secrets、shell 脚本。
Global Constraints
- 不自动
git commit(仓库规矩CLAUDE.md):本 plan 内所有git commit命令需用户明确批准后才执行。 - AI 不能吊销云端 key:轮换是运维动作。AI 无阿里云 / OpenAI / Dify / 微信小程序后台账号密码,不能登录控制台禁旧 key、签发新 key。即使代码已出库,旧 key 仍处于有效状态,任何 clone 到代码的人都可使用——轮换必须先于代码公开前完成。
- AI 不能强制推 git 历史:
git filter-repo --force会重写历史,需要团队协调(PR 关联的 fork、CI 缓存、协作者本地 reflog 全要清)。AI 只能生成执行命令清单与 README,不得git push --force到main/feat/uni等共享分支。 backend/.env不在本 plan 范围:是仓库根.gitignore已覆盖的本地文件(git check-ignore退出 0),不入 git;本地开发者各自管理。仅追踪文件才是本 plan 目标。backend/services/aiChatService/.env不在本 plan 范围:已被backend/.gitignore:21忽略,未追踪。k8s/helm/topfans/values-prod.yaml不在本 plan 范围:已被根.gitignore:61忽略,未追踪。模板values-prod.example.yaml仅含__FILL_ME__占位符,无密钥。- 占位符统一用
<REPLACE_ME>:.env.example里所有真实密钥(含OPENAI_API_KEY=sk-...DIFY_API_KEY=app-...MINIMAX_API_KEY=sk-...OSS_ACCESS_KEY_ID=LTAI...)一律替换为<REPLACE_ME>。非密钥配置项(如OPENAI_BASE_URL=https://api.weda.cc/v1OPENAI_MODEL=gpt-image-2)保留——它们是接入参数不是凭证。 - 每个任务结束做
git ls-files+grep扫描,确认密钥既不在跟踪里,也不在 commit 历史里(后者靠 filter-repo 验证)。 - 不引入新依赖:本 plan 不加任何 Go/Python 包。
前置(已确认的现状 — 实测于 feat/uni @ b7f8f1b)
| 文件 | 状态 | 泄露内容(行号) |
|---|---|---|
backend/deploy/envs/asset.env |
tracked | L10-11 OSS LTAI5t6QcdJHpYbCPxM8SXYE+Secret;L12 OSS_ROLE_ARN 含账号 ID |
backend/deploy/envs/user.env |
tracked | L21-22 同一套 OSS key 复用为 SMS key |
backend/deploy/envs/notification.env |
tracked | L22 PUSH_URL=https://env-00jy6bcqqwy6.dev-hz.cloudbasefunction.cn/sendMessage(注释明令「不要提交」) |
backend/deploy/envs/{activity,common,gallery,gateway,social}.env |
tracked | 当前 grep 未见明文密钥,但只要被跟踪,未来误改就被泄漏——一并出库 |
backend/.env.example |
tracked | L106 OPENAI_API_KEY=sk-proj-srKxybHaGxho...;L124 OPENAI_API_KEY=sk-eIOujD5rUug...(第二把)+ 微达配置;L131 DIFY_API_KEY=app-aHnBfMeOQp7A9dQneIFPdPaZ |
docker/.env |
tracked | L5 OPENAI_API_KEY=sk-eIOujD5rUug...;L8 DIFY_API_KEY=app-Ibs7reARanyuYGZ7zrLyiM6e |
docker/.env.local |
tracked | L21-22 同 OSS key |
docker/.env.prod |
tracked | L18-19 第二套 OSS LTAI5t99tafzfyrzbbEbjryH+Secret;L25 MiniMax sk-api-...;L47 OpenAI 微达;L52-53 同 SMS key;L63 注释掉的 Dify key |
k8s/helm/topfans/values-prod.yaml |
gitignored ✅ | L61 .gitignore: k8s/helm/**/values-prod.yaml 已生效(exit 0) |
k8s/helm/topfans/values-prod.example.yaml |
tracked ✅ | 仅 __FILL_ME__ 占位符,无密钥 |
k8s/helm/topfans/templates/secrets/{db,oss}-credentials.yaml |
tracked ✅ | Helm 模板,{{ .Values.secrets.* }} 引用,无明文 |
backend/services/aiChatService/.env |
gitignored ✅ | backend/.gitignore:21 覆盖 |
backend/.env(仓库根) |
gitignored ✅ | 根 .gitignore .env 覆盖 |
结论:8 个
backend/deploy/envs/*.env+docker/.env{,local,prod}+backend/.env.example= 12 个文件需在代码侧处理;其中8 个有真实密钥,**4 个(.env.example、common.env、activity.env、gallery.env、gateway.env、social.env)**只改.gitignore即可。
File Structure
- 修改
/Users/liulujian/Documents/code/TopFansByGithub/.gitignore— 在# statisticService区块后追加backend/deploy/envs/*.env、docker/.env*规则。 - 新建
/Users/liulujian/Documents/code/TopFansByGithub/docker/.gitignore— 当前不存在,兜底忽略*.env、*.env.*、!.env.example。 - 修改
/Users/liulujian/Documents/code/TopFansByGithub/backend/.gitignore— 追加deploy/envs/*.env(当前仅忽略.env、.env.local、.env.*.local,未覆盖deploy/envs/子目录下的.env)。 - 修改
/Users/liulujian/Documents/code/TopFansByGithub/backend/.env.example— 8 处真实密钥替换为<REPLACE_ME>(详见 Task 2)。 - 新建
/Users/liulujian/Documents/code/TopFansByGithub/docs/security/secrets-handling.md— 密钥管理 SOP(轮换周期、注入路径、应急流程)。给运维/新人参考。 - 新建
/Users/liulujian/Documents/code/TopFansByGithub/scripts/detect-secrets.sh— CI 友好扫描脚本:grep 跟踪文件里的sk-、app-、LTAI[A-Za-z0-9]{12,}、SM[0-9a-f]{32,}等模式,发现即 fail。供未来 PR check。
Task 1: .gitignore 加规则 + git rm --cached 删跟踪
Files:
- Modify:
/Users/liulujian/Documents/code/TopFansByGithub/.gitignore - Modify:
/Users/liulujian/Documents/code/TopFansByGithub/backend/.gitignore - Create:
/Users/liulujian/Documents/code/TopFansByGithub/docker/.gitignore
Interfaces:
-
跟踪视图变化:
git ls-files | grep -E 'deploy/envs/.*\.env$|^docker/\.env' | wc -l从 12 降到 0(或仅保留backend/.env.example)。 -
ignore 视图变化:
git check-ignore backend/deploy/envs/asset.env docker/.env.local从 exit 1 改为 exit 0。 -
Step 1: 备份当前跟踪清单
cd /Users/liulujian/Documents/code/TopFansByGithub
git ls-files | grep -E 'deploy/envs/.*\.env$|^docker/\.env|^backend/\.env\.example$' > /tmp/secrets_tracked_before.txt
cat /tmp/secrets_tracked_before.txt
Expected: 12 行(8 个 backend/deploy/envs/*.env + docker/.env + docker/.env.local + docker/.env.prod + backend/.env.example)。
- Step 2: 在根
.gitignore追加规则
在 /Users/liulujian/Documents/code/TopFansByGithub/.gitignore 末尾追加(保留换行):
# ============================================================
# 密钥与部署环境文件 (批次 0)
# - deploy/envs/*.env: 多机部署时由运维放到 /etc/topfans/*.env
# - docker/.env*: docker compose 部署时由 env_file 指向仓库外或 stdin 注入
# 真值不入 git,模板 (.example) 例外
# ============================================================
backend/deploy/envs/*.env
!backend/deploy/envs/*.env.example
docker/.env
docker/.env.local
docker/.env.prod
docker/.env.*
!docker/.env.example
- Step 3: 验证根
.gitignore新规则生效
cd /Users/liulujian/Documents/code/TopFansByGithub
git check-ignore -v backend/deploy/envs/asset.env docker/.env.local docker/.env.prod docker/.env 2>&1
echo "exit=$?"
Expected: 每行打印 .gitignore:<行号>:... <path>,exit code = 0(之前 exit 1)。
- Step 4: 在
backend/.gitignore追加deploy/envs/*.env(冗余但保底)
在 /Users/liulujian/Documents/code/TopFansByGithub/backend/.gitignore 末尾追加:
# 部署用私有环境文件 (批次 0)
deploy/envs/*.env
!deploy/envs/*.env.example
即使子目录有自己的
.gitignore,保留这一行作为冗余防御(防止某天根.gitignore被裁剪)。
- Step 5: 新建
docker/.gitignore
新建 /Users/liulujian/Documents/code/TopFansByGithub/docker/.gitignore,内容:
# docker compose 环境文件 (批次 0)
# 真值不入 git;模板 .env.example 例外
.env
.env.local
.env.prod
.env.*
!.env.example
- Step 6: 验证
backend/.gitignore新规则生效
cd /Users/liulujian/Documents/code/TopFansByGithub
git check-ignore -v backend/deploy/envs/asset.env 2>&1
echo "exit=$?"
Expected: exit 0(被 backend/.gitignore 命中;也可能只被根规则命中,正常)。
- Step 7:
git rm --cached移除跟踪(本地文件保留)
⚠️ 本步骤需要用户明确批准后执行:会修改 git 索引。
cd /Users/liulujian/Documents/code/TopFansByGithub
git rm --cached -r backend/deploy/envs/
git rm --cached docker/.env docker/.env.local docker/.env.prod
# 注意:backend/.env.example 是模板,不删!
git status --short | head -20
Expected: 12 个文件标记为 D(deleted from index),但磁盘文件保留。backend/.env.example 不在删除清单里。
- Step 8: 验证跟踪清单已清空
cd /Users/liulujian/Documents/code/TopFansByGithub
git ls-files | grep -E 'deploy/envs/.*\.env$|^docker/\.env$' > /tmp/secrets_tracked_after.txt
diff /tmp/secrets_tracked_before.txt /tmp/secrets_tracked_after.txt
echo "remaining_lines=$(wc -l < /tmp/secrets_tracked_after.txt)"
Expected: diff 列出全部 12 个被删的路径;remaining_lines=0。注意:backend/.env.example 因为匹配模式不含 \.env\.example$,仍在跟踪(用 git ls-files backend/.env.example 应仍打印 1 行)。
- Step 9: 验证磁盘文件还在
cd /Users/liulujian/Documents/code/TopFansByGithub
ls backend/deploy/envs/*.env | wc -l # 期望: 8
ls docker/.env docker/.env.local docker/.env.prod 2>&1
Expected: 8 个 .env 仍存在于磁盘;docker/.env{,.local,.prod} 3 个文件存在。
- Step 10: 暂存(用户批准后 commit)
cd /Users/liulujian/Documents/code/TopFansByGithub
git add .gitignore backend/.gitignore docker/.gitignore
git status --short
Expected: 3 个 .gitignore 修改处于 staged 状态,外加 12 个 D(来自 Step 7)。
Commit 命令(用户批准后):
git commit -m "chore(security): untrack deployed env files & ignore patterns (batch 0) - .gitignore: add backend/deploy/envs/*.env, docker/.env* - backend/.gitignore: add deploy/envs/*.env (defense in depth) - docker/.gitignore: new file, ignore docker compose env files - git rm --cached: 12 tracked env files (8 deploy/envs + 3 docker/.env*) - 详见 docs/superpowers/plans/2026-07-21-secrets-remediation.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
Task 2: backend/.env.example 真实密钥替换为占位符
Files:
- Modify:
/Users/liulujian/Documents/code/TopFansByGithub/backend/.env.example
Interfaces:
-
替换前:8 行含真实密钥。
-
替换后:所有真值 =
<REPLACE_ME>;非密钥配置(endpoint、region、bucket、模型名)保留。 -
Step 1: 备份原文件
cd /Users/liulujian/Documents/code/TopFansByGithub
cp backend/.env.example /tmp/backend.env.example.bak
md5sum /tmp/backend.env.example.bak
Expected: 记录 md5(如 d41d8cd98f00b204e9800998ecf8427e)作为基线。
- Step 2: 替换 L106
OPENAI_API_KEY(OpenAI 直连那把 sk-proj-...)
将:
OPENAI_API_KEY=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
替换为:
OPENAI_API_KEY=<REPLACE_ME>
注释行 # 必填:OpenAI API Key... 保留。
- Step 3: 替换 L124 第二把 OpenAI key + 微达 endpoint 配置
L124 重复声明
OPENAI_API_KEY(覆盖 L106),保留这把作为 LASER_GEN_PROVIDER=openai 时的实际使用 key。
将:
OPENAI_API_KEY=sk-eIOujD5rUugIRIPecFi3I2rFr6Bhxx1jsRzRm6phyNeeKrCI
# 微达API BaseURL(必须含 /v1 后缀,代码会拼成 /v1/images/edits)
OPENAI_BASE_URL=https://api.weda.cc/v1
# 中转站实际暴露的 image 模型
OPENAI_MODEL=gpt-image-2
替换为:
OPENAI_API_KEY=<REPLACE_ME>
# 微达API BaseURL(必须含 /v1 后缀,代码会拼成 /v1/images/edits)
OPENAI_BASE_URL=https://api.weda.cc/v1
# 中转站实际暴露的 image 模型
OPENAI_MODEL=gpt-image-2
注释 # 微达API BaseURL... # 中转站... 保留(是接入说明不是凭证);endpoint URL 与 model 名保留(非敏感)。
- Step 4: 替换 L131
DIFY_API_KEY
将:
DIFY_API_KEY=app-aHnBfMeOQp7A9dQneIFPdPaZ
替换为:
DIFY_API_KEY=<REPLACE_ME>
L132 DIFY_API_BASE=http://localhost/v1 与 L133 DIFY_TIMEOUT_SEC=60 保留。
- Step 5: 扫一遍确认无遗漏的 sk-/app-/LTAI 模式
cd /Users/liulujian/Documents/code/TopFansByGithub
grep -nE 'sk-(proj-|api-|[A-Za-z0-9]{20,})|app-[A-Za-z0-9]{16,}|LTAI[A-Za-z0-9]{12,}|sk-cp-[A-Za-z0-9-]+' backend/.env.example || echo "clean"
Expected: 仅命中占位符 <REPLACE_ME> 所在行(如有),且模式后面跟的不是真值。任何前缀匹配 sk-proj- / sk-api- / app-... / LTAI... 后面跟 ≥12 字符的都视为泄漏。
- Step 6: 确认
.env.example仍是合法 shell 格式(语法检查)
cd /Users/liulujian/Documents/code/TopFansByGithub
# 用 docker 容器跑一次 dotenv parser 验证语法
docker run --rm -v "$PWD/backend/.env.example:/tmp/.env" python:3.11-slim bash -c "
pip install -q python-dotenv >/dev/null
python3 -c \"from dotenv import dotenv_values; d=dotenv_values('/tmp/.env'); print(f'parsed {len(d)} keys'); print('has REPLACE_ME:', '<REPLACE_ME>' in str(d.values()))\""
Expected: parsed N keys (N 应与原文件相同,约 60-70);has REPLACE_ME: True。
- Step 7: 验证 Go 代码仍能读到
OPENAI_API_KEY等 key 名(占位符不破坏 key 名)
cd /Users/liulujian/Documents/code/TopFansByGithub
grep -rn "os.Getenv(\"OPENAI_API_KEY\")" backend/ 2>/dev/null | head -5
grep -rn "os.Getenv(\"DIFY_API_KEY\")" backend/ 2>/dev/null | head -5
Expected: 至少各 1 行命中——证明 key 名仍被代码读取,运维部署时只需要注入真值即可。
- Step 8: 暂存(用户批准后 commit)
cd /Users/liulujian/Documents/code/TopFansByGithub
git add backend/.env.example
git diff --cached backend/.env.example | head -40
Expected: 仅显示 4 处 =sk-... / =app-... → =<REPLACE_ME> 的变更,无 endpoint/model 变更。
Commit 命令(用户批准后):
git commit -m "chore(env): replace leaked secrets in .env.example with placeholders (batch 0) - OPENAI_API_KEY (2 处, 含微达中转站) → <REPLACE_ME> - DIFY_API_KEY → <REPLACE_ME> - 保留 endpoint/model/region/bucket 等非凭证配置 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
Task 3: 端到端验证 — 跟踪里再无密钥
Files: 无(验证任务)
- Step 1:
git ls-files扫描(跟踪层)
cd /Users/liulujian/Documents/code/TopFansByGithub
git ls-files | xargs grep -lE 'sk-(proj-|api-)[A-Za-z0-9]{20,}|app-[A-Za-z0-9]{16,}|LTAI[A-Za-z0-9]{12,}|SMS_314621237' 2>/dev/null
echo "exit=$?"
Expected: 空输出,exit=1(grep 无匹配返回 1)。任何输出都是遗漏。
- Step 2: 工作区扫描(含 gitignore 后的未跟踪文件)
cd /Users/liulujian/Documents/code/TopFansByGithub
# 扫描所有 .env.example / .env / yaml / json / go(排除 vendor/node_modules)
grep -rEn 'sk-(proj-|api-)[A-Za-z0-9]{20,}|app-[A-Za-z0-9]{16,}|LTAI[A-Za-z0-9]{12,}' \
--include='*.env' --include='*.env.example' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.go' \
--exclude-dir=node_modules --exclude-dir=unpackage --exclude-dir=.git --exclude-dir=vendor \
. 2>/dev/null
echo "exit=$?"
Expected: 空输出,exit=1。backend/deploy/envs/*.env 与 docker/.env* 仍含真值但已被 gitignore(Step 2 不跟踪它们,但仍在磁盘,扫描仍可能命中——需要人工判断是否命中在 gitignore 列表里)。
实际预期会命中
backend/deploy/envs/asset.envdocker/.env.prod等(因为扫描是文件层非 git 层)。通过 = 命中文件全部在.gitignore名单里。
- Step 3: 全仓 Go 编译确认未引入新依赖
cd /Users/liulujian/Documents/code/TopFansByGithub/backend
go build ./... 2>&1 | tail -20
Expected: 无错误(本 plan 不改 Go 代码,仅改 .gitignore、.env.example,应无影响)。
- Step 4: 确认
.env.example仍能被 Go 代码读到 key
跑一个 5 行 Go 小程序读 env,确认占位符能被正确解析为字符串
<REPLACE_ME>:
cd /Users/liulujian/Documents/code/TopFansByGithub/backend
cat > /tmp/check_env.go <<'EOF'
package main
import ("fmt"; "os"; "github.com/joho/godotenv")
func main() {
_ = godotenv.Load(".env.example")
for _, k := range []string{"OPENAI_API_KEY","DIFY_API_KEY","MINIMAX_API_KEY","JWT_SECRET","SECRET_KEY"} {
v := os.Getenv(k); if v == "" { v = "<empty>" }
fmt.Printf("%s=%s\n", k, v)
}
}
EOF
cd /tmp && go mod init checkenv 2>/dev/null; go get github.com/joho/godotenv 2>/dev/null
go run /tmp/check_env.go
cd /Users/liulujian/Documents/code/TopFansByGithub
Expected: 输出 5 行,至少 OPENAI_API_KEY=<REPLACE_ME> 与 DIFY_API_KEY=<REPLACE_ME> 出现;JWT_SECRET 等原本就为空的不报错。
Task 4: 部署消费方迁移模板(AI 可做骨架,运维填真值)
Files:
- Modify:
/Users/liulujian/Documents/code/TopFansByGithub/docker/docker-compose.local.yml - Modify:
/Users/liulujian/Documents/code/TopFansByGithub/docker/docker-compose.prod.yml - Modify:
/Users/liulujian/Documents/code/TopFansByGithub/backend/dev.sh
Interfaces:
-
部署时密钥不再从仓库内
docker/.env*读取,改从env_file:指向宿主机/etc/topfans/secrets.env或docker run -e KEY=val命令行注入。 -
Step 1: 读当前 docker-compose 真值引用
cd /Users/liulujian/Documents/code/TopFansByGithub
grep -nE 'env_file|environment' docker/docker-compose.local.yml | head -20
grep -nE 'env_file|environment' docker/docker-compose.prod.yml | head -20
Expected: 当前用 env_file: - ./.env 或 env_file: - ./.env.prod 引用仓库内文件。
- Step 2: 改
docker-compose.local.yml的 env_file 路径
将所有 env_file: 引用从 ./.env / ./.env.local 改为宿主机注入路径:
# 旧
env_file:
- ./.env
# 新(密钥不入仓库;真值由部署方提供 .env 或 stdin 注入)
env_file:
- path: ./docker/.env.example # 仅占位模板,所有密钥为 <REPLACE_ME>
- path: /run/secrets/topfans-local.env # 部署时由 docker secret / bind mount 注入
required: false
保留
./docker/.env.example作为「key 名清单 + 占位符」,让本地docker compose up不会因缺 env 报错;真值文件由开发者自己放在/run/secrets/topfans-local.env或~/.config/topfans/local.env。
- Step 3: 改
docker-compose.prod.yml同理
# 旧
env_file:
- ./.env.prod
# 新
env_file:
- path: /etc/topfans/docker.env # 宿主机 /etc/topfans/docker.env
required: true
并在文件顶部加注释:
# ⚠️ 生产密钥不入 git
# 部署方需在宿主机准备 /etc/topfans/docker.env,包含所有 <REPLACE_ME> 对应的真值
# 示例: openssl rand -hex 32 # JWT_SECRET
- Step 4: 改
backend/dev.sh的密钥加载逻辑
读 backend/dev.sh 的 env 加载段(grep \.env\|source\|export),将所有 source backend/.env 改为:
# 加载仓库内 .env.example 作为 key 清单(真值留空)
[ -f backend/.env.example ] && set -a && source backend/.env.example && set +a
# 真值从仓库外注入(优先级更高)
[ -f ~/.config/topfans/dev.env ] && set -a && source ~/.config/topfans/dev.env && set +a
[ -f /etc/topfans/dev.env ] && set -a && source /etc/topfans/dev.env && set +a
- Step 5: 验证 docker-compose 仍能解析
cd /Users/liulujian/Documents/code/TopFansByGithub
docker compose -f docker/docker-compose.local.yml --env-file docker/.env.example config 2>&1 | head -30
Expected: 正常输出(占位符环境能解析,service 定义无语法错误);可能因 /run/secrets/... 不存在而 warn,可忽略。
- Step 6: Commit(用户批准后)
Commit 命令(用户批准后):
git add docker/docker-compose.local.yml docker/docker-compose.prod.yml backend/dev.sh git commit -m "chore(deploy): migrate env_file to out-of-repo secret injection (batch 0) - docker-compose.{local,prod}.yml: env_file 改读 /etc/topfans/*.env - backend/dev.sh: 加载 ~/.config/topfans/dev.env 真值 - .env.example 仅作 key 名 + 占位符 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
Task 5: 密钥扫描 CI 守护脚本(防止再次入仓)
Files:
- Create:
/Users/liulujian/Documents/code/TopFansByGithub/scripts/detect-secrets.sh
Interfaces:
-
退出码 0 = 干净;非 0 = 命中可疑密钥模式。供未来
pre-commithook / CI 步骤调用。 -
Step 1: 写
scripts/detect-secrets.sh
新建 /Users/liulujian/Documents/code/TopFansByGithub/scripts/detect-secrets.sh:
#!/usr/bin/env bash
# detect-secrets.sh — 扫描仓库内跟踪文件的高熵 / 已知密钥前缀
# 退出码: 0 = 无命中, 1 = 命中
# 排除: node_modules, unpackage, .git, vendor, frontend/.env.{development,production}
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# 模式: OpenAI / Dify / 阿里云 RAM 子账号 AccessKey / 通用 high-entropy sk- 前缀
PATTERNS=(
'sk-proj-[A-Za-z0-9_-]{20,}'
'sk-api-[A-Za-z0-9_-]{20,}'
'sk-cp-[A-Za-z0-9_-]{20,}'
'app-[A-Za-z0-9]{16,}'
'LTAI[A-Za-z0-9]{12,}'
'AKID[A-Za-z0-9]{16,}'
)
EXCLUDE_DIRS=(
'--exclude-dir=node_modules'
'--exclude-dir=unpackage'
'--exclude-dir=.git'
'--exclude-dir=vendor'
'--exclude-dir=.code-review-graph'
'--exclude-dir=.superpowers'
'--exclude-dir=.agents'
'--exclude-dir=.claude'
'--exclude-dir=frontend/dist'
'--exclude-dir=frontend/.hbuilderx'
)
# 允许的占位符豁免(命中后仍 fail,但提示是占位符)
PLACEHOLDER='<REPLACE_ME>'
INCLUDE_FILES=(
'--include=*.env'
'--include=*.env.example'
'--include=*.yaml'
'--include=*.yml'
'--include=*.json'
'--include=*.go'
'--include=*.py'
'--include=*.js'
'--include=*.ts'
'--include=*.sh'
)
FOUND=0
for pat in "${PATTERNS[@]}"; do
matches=$(grep -rEn "${EXCLUDE_DIRS[@]}" "${INCLUDE_FILES[@]}" "$pat" . 2>/dev/null \
| grep -v "$PLACEHOLDER" || true)
if [ -n "$matches" ]; then
echo "=== PATTERN: $pat ===" >&2
echo "$matches" >&2
FOUND=1
fi
done
if [ "$FOUND" -eq 1 ]; then
echo "" >&2
echo "❌ 检测到可能的密钥泄露,请检查上方命中并替换为 <REPLACE_ME> 占位符。" >&2
echo " 真值请通过部署侧 / KMS / Docker secret 注入,不要提交到 git。" >&2
exit 1
fi
echo "✅ 无密钥泄露"
exit 0
- Step 2: 加执行权限
chmod +x /Users/liulujian/Documents/code/TopFansByGithub/scripts/detect-secrets.sh
- Step 3: 在干净仓库上跑(应 pass)
cd /Users/liulujian/Documents/code/TopFansByGithub
./scripts/detect-secrets.sh
echo "exit=$?"
Expected: ✅ 无密钥泄露 + exit=0。
- Step 4: 构造一个假阳性用例确认能 catch
cd /Users/liulujian/Documents/code/TopFansByGithub
echo "OPENAI_API_KEY=sk-proj-fakefakefakefakefakefakefakefake" > /tmp/leak_test.env
cd /tmp && mkdir -p leak_check && cp /tmp/leak_test.env leak_check/.env
# 在仓库内临时建一个测试文件
echo "DIFY_API_KEY=app-fakefakefakefakefake" > backend/.env.test_leak
cd /Users/liulujian/Documents/code/TopFansByGithub
./scripts/detect-secrets.sh
echo "exit=$?"
# 清理
rm -f backend/.env.test_leak /tmp/leak_test.env
rm -rf /tmp/leak_check
cd /Users/liulujian/Documents/code/TopFansByGithub
Expected: 打印 ❌ 检测到可能的密钥泄露 并 exit 1;清理后再次运行应 pass。
- Step 5: 在
.gitignore排除脚本自身(避免被误扫描)
scripts/ 不在 EXCLUDE_DIRS 里,OK。但若需排除 scripts/detect-secrets.sh 本身的正则字面量,加到 exclude:
无需修改(模式字面量是元字符串,grep -E 不会匹配)。
- Step 6: Commit(用户批准后)
Commit 命令(用户批准后):
git add scripts/detect-secrets.sh git commit -m "ci(security): add detect-secrets.sh for tracking file scanning (batch 0) 扫描 sk-proj-* / sk-api-* / app-* / LTAI* 等已知密钥前缀 供 pre-commit / CI 步骤调用,命中即 fail Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
Task 6: 运维交付物 — 密钥轮换 SOP 文档(AI 起草,运维审阅)
Files:
- Create:
/Users/liulujian/Documents/code/TopFansByGithub/docs/security/secrets-handling.md
Interfaces:
-
文档涵盖:每类密钥的轮换入口、轮换步骤、消费方清单、回滚方案、紧急吊销流程。
-
本文档是给运维的执行清单,不是代码变更。AI 起草后由运维校对。
-
Step 1: 起草 SOP 文档
新建 /Users/liulujian/Documents/code/TopFansByGithub/docs/security/secrets-handling.md,内容:
# 密钥管理 SOP (批次 0)
> 本文档定义 TopFans 仓库所有生产密钥的**轮换周期、注入路径、紧急吊销流程**。
> 配套实施见 `docs/superpowers/plans/2026-07-21-secrets-remediation.md`。
## 1. 密钥清单
| 密钥 | 当前来源(仓库已删) | 真值存放 | 轮换周期 | 紧急吊销入口 |
|------|----------------------|----------|----------|--------------|
| 阿里云 OSS AccessKey | `backend/deploy/envs/asset.env` L10-11 + `user.env` L21-22 + `docker/.env.prod` L18-19 | K8s Secret `oss-credentials` / `/etc/topfans/asset.env` | 90 天 | https://ram.console.aliyun.com → 用户 → AccessKey → 禁用 |
| SMS AccessKey | 同 OSS(同一账号) | 同 OSS 注入路径 | 90 天 | 同上 |
| uniPush URL | `backend/deploy/envs/notification.env` L22 | K8s Secret `notification-config` | 180 天 | 微信小程序后台 → uniCloud → sendMessage 重置 |
| OpenAI `sk-proj-...` | `backend/.env.example` L106 | K8s Secret `ai-secrets.openaiApiKey` | 90 天 | https://platform.openai.com/api-keys → Revoke |
| 微达中转 `sk-...` | `backend/.env.example` L124 + `docker/.env.prod` L47 | 同上 | 90 天 | 联系微达客服 |
| Dify App API Key | `backend/.env.example` L131 + `docker/.env.prod` L63 | K8s Secret `ai-secrets.difyApiKey` | 90 天 | Dify 控制台 → 工作室 → API 密钥 → 重置 |
| MiniMax API Key | `docker/.env.prod` L25 | K8s Secret `ai-secrets.minimaxApiKey` | 180 天 | https://platform.MiniMax.com → API Keys |
| JWT_SECRET | `docker/.env.prod` L12 | K8s Secret `jwt-secret` | **谨慎**(改了全站 token 失效) | — |
| SECRET_KEY (周边 HMAC) | `docker/.env.prod` L66 | K8s Secret `peripheral-secret` | 仅泄露时 | — |
## 2. 轮换流程(以 OSS 为例)
1. 阿里云 RAM 控制台创建新子账号 AccessKey,绑定 `top-fans-oss-user` 角色。
2. 在 K8s Secret `oss-credentials` 更新 `OSS_ACCESS_KEY_ID` / `OSS_ACCESS_KEY_SECRET`:
```bash
kubectl -n topfans edit secret oss-credentials
- 触发滚动重启:
kubectl -n topfans rollout restart deployment/asset-service deployment/user-service。 - 观察 5 分钟日志确认 OSS 调用无 403。
- 24 小时观察期后回到阿里云控制台禁用旧 key。
- 同步更新
docs/security/secrets-handling.md表格的「当前来源」列。
3. 紧急吊销(疑似泄露)
立即在云控制台 Disable 旧 key(不要先 Delete,留 7 天观察)。同时:
# 1. 标记泄露事件
echo "[$(date -Iseconds)] OSS key leaked, rotating to new AKID-NEW" >> /var/log/topfans/secret_rotation.log
# 2. 通知开发 / 运维群
# (此处加飞书/钉钉 webhook 调用)
4. 注入路径核对清单
- K8s: 所有
*.env.example占位符在values-prod.yaml已替换为真值(不入 git) - Docker compose:
/etc/topfans/docker.env存在且权限 600 - 本地开发:
~/.config/topfans/dev.env存在 - CI: 密钥走 GitHub Actions Secret / 阿里云 ACR CredentialHelper,不走 env 明文
5. git 历史清理(仅在密钥真值泄露到 git 历史时执行)
⚠️ 本节由运维主导,AI 不可执行 git push --force。
# 安装 git-filter-repo
pip install git-filter-repo
# 备份裸仓库(防止 filter-repo 出错)
cp -r /path/to/topfans.git /tmp/topfans.git.backup
# 删除敏感路径历史
cd /path/to/topfans
git filter-repo --invert-paths \
--path backend/deploy/envs/ \
--path docker/.env \
--path docker/.env.local \
--path docker/.env.prod \
--force
# 强推到远端(团队公告后执行)
git remote add origin git@github.com:org/topfans.git
git push origin --force --all
git push origin --force --tags
# 全员重新 clone
6. 历史教训(README 引用)
- 2026-07-21 审计发现 P0-1:8 个
backend/deploy/envs/*.env+docker/.env{,local,prod}+backend/.env.example11 处真实密钥在 git 跟踪中。批次 0 修复后,所有密钥改走仓库外注入。 - 教训:
.env.example不等于.env,跟踪前者 OK,跟踪后者永远错。模板里写密钥原文也是错——必须写占位符。
- [ ] **Step 2: Commit(用户批准后,运维审阅后)**
> **Commit 命令(用户批准后)**:
> ```bash
> git add docs/security/secrets-handling.md
> git commit -m "docs(security): secrets handling SOP (batch 0)
>
> 密钥清单 + 轮换流程 + 紧急吊销 + 注入路径 + git 历史清理
>
> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
> ```
---
## Task 7: 运维交付物 — 云端密钥轮换(**AI 不可执行,仅核查清单**)
> ⚠️ **本任务完全由运维执行**。AI 无云控制台账号权限,不能登录阿里云/OpenAI/Dify 禁用旧 key。
> 本节是给运维的核查清单,AI 仅做文档化与验收检查。
**Files:** 无(运维在云控制台操作)
**Interfaces:**
- 验收:每把旧 key 在云控制台状态 = `Disabled`;新 key 已在所有消费方生效。
- [ ] **Step 1: 运维轮换 OSS AccessKey(两套)**
| 旧 AKID | 云端操作 | 新 AKID 注入位置 |
|---------|----------|------------------|
| `LTAI5t6QcdJHpYbCPxM8SXYE`(`asset.env`+`user.env`) | 阿里云 RAM 禁用 | K8s `oss-credentials.secret` + `/etc/topfans/asset.env` + `/etc/topfans/user.env` |
| `LTAI5t99tafzfyrzbbEbjryH`(`docker/.env.prod`) | 同上 | K8s `oss-credentials.secret` + 宿主机 `/etc/topfans/docker.env` |
AI 验收命令:
```bash
# 在本地抓包验证 OSS 调用是否带新 AKID(运维新签发的)
tcpdump -i any -A -s0 'host oss-cn-shanghai.aliyuncs.com and tcp port 80' 2>&1 | grep -E 'Authorization.*OSS' | tail -5
或者通过云控制台「访问日志」筛 OSS 桶 7 天内的请求源 IP/Key。
- Step 2: 运维轮换 OpenAI
sk-proj-...(直连那把)
控制台:https://platform.openai.com/api-keys → Revoke 旧 key → Create new key。
- Step 3: 运维轮换微达
sk-eIOujD5rUug...
联系微达客服或控制台(如有)→ 旧 key 禁用 → 新 key 注入 K8s ai-secrets.openaiApiKey。
- Step 4: 运维轮换 Dify
app-aHnBfMeOQp7A9dQneIFPdPaZ
Dify 控制台 → 工作室 → 角角 → API 密钥 → 重置。
- Step 5: 运维轮换 uniPush URL(云函数)
微信小程序后台 → uniCloud → sendMessage 云函数 → 重新部署(URL 会变)或保留 URL 重置签名。
- Step 6: 运维轮换 MiniMax
sk-api-...
https://platform.MiniMax.com → API Keys → 旧 key 禁用 → 新 key 注入 K8s ai-secrets.minimaxApiKey。
- Step 7: 运维轮换 SMS AccessKey
阿里云 SMS 控制台 → AccessKey 管理(同一 RAM 子账号)→ 禁用旧 → 创建新。
- Step 8: AI 验收 —
detect-secrets.sh干净 + 旧 key 已从消费方移除
cd /Users/liulujian/Documents/code/TopFansByGithub
./scripts/detect-secrets.sh # 应 exit 0
# 验证旧 key 不再出现在 K8s secret / docker compose env
kubectl -n topfans get secret oss-credentials -o jsonpath='{.data.OSS_ACCESS_KEY_ID}' | base64 -d
# 应输出 NEW AKID,不是 LTAI5t6QcdJHpYbCPxM8SXYE 也不是 LTAI5t99tafzfyrzbbEbjryH
Task 8: 运维交付物 — git 历史清理与全员重 clone(AI 不可执行)
⚠️ 本任务完全由运维执行。AI 没有
git push --force到共享分支的权限,也无团队协调能力。 本节是给运维的命令清单,AI 仅做文档化。
Files: 无(git 操作)
- Step 1: 备份裸仓库
# 运维在本地做
cp -r /Users/liulujian/Documents/code/TopFansByGithub/.git /tmp/topfans.git.backup.$(date +%Y%m%d)
- Step 2: 安装 git-filter-repo
pip install git-filter-repo # 或 brew install git-filter-repo
- Step 3: 删除历史中的密钥路径
cd /Users/liulujian/Documents/code/TopFansByGithub
git filter-repo --invert-paths \
--path backend/deploy/envs/ \
--path docker/.env \
--path docker/.env.local \
--path docker/.env.prod \
--force
- Step 4: 验证历史已清
cd /Users/liulujian/Documents/code/TopFansByGithub
git log --all --full-history -- backend/deploy/envs/asset.env | head -5
echo "expected: empty"
git log --all --full-history -p -- backend/.env.example | grep -c 'sk-proj' || true
echo "expected: 0"
- Step 5: 团队公告 + 强推
运维需在群里公告:历史已重写,所有人必须
rm -rf本地仓库后git clone。 CI 缓存需清:GitHub Actions cache、阿里云 ACR build cache、Slack/Notion 镜像备份。
git remote add origin git@github.com:zerosaturation/topfans.git # 按实际
git push origin --force --all
git push origin --force --tags
- Step 6: 全员重 clone(运维通知,不在 AI 范围)
# 开发者本地
rm -rf /Users/liulujian/Documents/code/TopFansByGithub
cd ~/Documents/code
git clone git@github.com:zerosaturation/topfans.git TopFansByGithub
- Step 7: AI 验收 — 历史无密钥残留
cd /Users/liulujian/Documents/code/TopFansByGithub
# 跟踪 + 历史 + 工作区三层扫描
git rev-list --all | xargs -I{} git show {}:backend/deploy/envs/asset.env 2>/dev/null | head
echo "expected: empty (no commit shows asset.env anymore)"
Self-Review
- Spec 覆盖:
- 批次 0 §「密钥出库」= Task 1(
.gitignore+git rm --cached)+ Task 2(.env.example占位符)= 完整覆盖。 - 批次 0 §「轮换」= Task 7(运维 SOP,AI 仅核查)= 运维主导,文档化。
- 批次 0 §「git 历史清理」= Task 8(运维执行,AI 仅核查)= 运维主导,文档化。
- 部署消费方迁移 = Task 4(AI 做骨架)= 完整覆盖。
- 防止再次泄漏 = Task 5(
detect-secrets.sh)+ Task 6(SOP 文档)= 预防措施。
- 批次 0 §「密钥出库」= Task 1(
- Placeholder 扫描:
- 无 TBD;
.env.example占位符统一<REPLACE_ME>。 - 脚本、SOP、commit message 均给出完整文本。
- 无 TBD;
- 全局约束一致性:
- 每个 commit 步骤标 "用户批准后"(CLAUDE.md 强制)。
- 运维步骤明确标 "AI 不可执行"。
- 不引入新依赖(仅 shell 脚本 + git/grep 已装)。
- 跨文件一致性:
- Task 1 的
.gitignore规则 vs Task 5 的EXCLUDE_DIRS一致(都排除.code-review-graph.superpowers.agents.claude)。 - Task 2 的 4 处替换 vs Task 7 的密钥清单 8 项 = 模板覆盖 4 处真值,部署侧 8 项全列。
- Task 4 的 env_file 路径
/etc/topfans/docker.env与 Task 6 的 SOP 「注入路径核对清单」一致。
- Task 1 的
- 风险标注:
- Task 4 Step 5 的
docker compose config可能因/run/secrets不存在而 warn,非阻塞。 - Task 5 Step 4 的假阳性测试需手工清理
backend/.env.test_leak,已提醒。 - Task 7/8 完全运维主导,AI 仅生成验收命令。
- Task 4 Step 5 的
- 失败重做路径:
- 若 Task 1 Step 8 的
git ls-files仍有命中,回滚git rm --cached用git reset HEAD backend/deploy/envs/asset.env然后重新跑。 - 若 Task 5 在干净仓库上 fail,说明
EXCLUDE_DIRS不全,需补全。
- 若 Task 1 Step 8 的