topfans/docs/superpowers/plans/2026-07-21-secrets-remediation.md
2026-07-21 21:14:22 +08:00

38 KiB
Raw Blame History

密钥出库与轮换 (批次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 加进 .gitignoregit rm --cached 删跟踪;backend/.env.example 真实密钥替换为 <REPLACE_ME> 占位符;创建 docker/.gitignore(当前不存在)。
  • 运维侧AI 不可做)
    1. 云端轮换:阿里云 RAM 子账号 AccessKey、OpenAI/微达 中转站 API Key、Dify App API Key、uniPush URL/TOKEN、SMS AccessKey——在云控制台禁用旧 key、签发新 key、更新所有部署消费方。
    2. git 历史清理git filter-repo --invert-paths 删历史记录,强推 + 全员 rm -rf && git clone
  • 部署消费方AI 可做模板):更新 docker-compose.{local,prod}.ymlk8s/helm/topfans/values-prod.example.yamlbackend/dev.sh 的密钥注入方式env_file 指向仓库外、或 helm secret 模板从外部 Secret 引用)。

Tech Stack: Gitgit rm --cachedgit 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 --forcemain/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/v1 OPENAI_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+SecretL12 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+SecretL25 MiniMax sk-api-...L47 OpenAI 微达L52-53 同 SMS keyL63 注释掉的 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/*.envdocker/.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 -l12 降到 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 0backend/.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 个文件标记为 Ddeleted 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: 记录 md5d41d8cd98f00b204e9800998ecf8427e)作为基线。

  • Step 2: 替换 L106 OPENAI_API_KEYOpenAI 直连那把 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=1grep 无匹配返回 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/*.envdocker/.env* 仍含真值但已被 gitignoreStep 2 不跟踪它们,但仍在磁盘,扫描仍可能命中——需要人工判断是否命中在 gitignore 列表里)。

实际预期会命中 backend/deploy/envs/asset.env docker/.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.envdocker 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: - ./.envenv_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-commit hook / 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_DIRSOK。但若需排除 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
  1. 触发滚动重启:kubectl -n topfans rollout restart deployment/asset-service deployment/user-service
  2. 观察 5 分钟日志确认 OSS 调用无 403。
  3. 24 小时观察期后回到阿里云控制台禁用旧 key。
  4. 同步更新 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-18 个 backend/deploy/envs/*.env + docker/.env{,local,prod} + backend/.env.example 11 处真实密钥在 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 历史清理与全员重 cloneAI 不可执行

⚠️ 本任务完全由运维执行。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 cloneCI 缓存需清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运维 SOPAI 仅核查)= 运维主导,文档化。
    • 批次 0 §「git 历史清理」= Task 8运维执行AI 仅核查)= 运维主导,文档化。
    • 部署消费方迁移 = Task 4AI 做骨架)= 完整覆盖
    • 防止再次泄漏 = Task 5detect-secrets.sh+ Task 6SOP 文档)= 预防措施
  • Placeholder 扫描
    • 无 TBD.env.example 占位符统一 <REPLACE_ME>
    • 脚本、SOP、commit message 均给出完整文本。
  • 全局约束一致性
    • 每个 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 4 Step 5 的 docker compose config 可能因 /run/secrets 不存在而 warn非阻塞。
    • Task 5 Step 4 的假阳性测试需手工清理 backend/.env.test_leak,已提醒。
    • Task 7/8 完全运维主导AI 仅生成验收命令。
  • 失败重做路径
    • 若 Task 1 Step 8 的 git ls-files 仍有命中,回滚 git rm --cachedgit reset HEAD backend/deploy/envs/asset.env 然后重新跑。
    • 若 Task 5 在干净仓库上 fail说明 EXCLUDE_DIRS 不全,需补全。