Files
group_fqcd_jr/开发文档/D3.8-模型密钥轮换与凭据安全操作手册-2026-09-20.md
张胜宇 c91bbcbdc1 feat(ops)+docs: 密钥轮换工具 + 两份文档目录审计收口(D1.1 §23 / D2.1 v6.26)
一、密钥轮换(新增工具 + 操作手册)
- 新增 tools/rotate_api_keys.py:--check 体检 + 交互式轮换;getpass 不回显、
  自动备份 .env.bak-<时间戳>(已被 ignore 命中)、校验不过整体不写入、
  三个 Qwen 变量写同一值 / 两个 DeepSeek 变量写同一值。
  实测 --check:Qwen 三变量同值且非空、DeepSeek 两变量同值且非空。
- 新增 开发文档/D3.8-模型密钥轮换与凭据安全操作手册-2026-09-20.md(CS-OPS-2026-023):
  .env 5 个变量与读取方取证、五步流程、3 个坑、复核清单、回退方式、能力边界。
- 口径确认:model_endpoint_config.secret_ref 存变量名 ⇒ 轮换只改 .env,不动 DB;
  但必须重启 API + Worker。

二、门禁修复:docs/ 编号撞车
- tools/check_authoritative_docs.py(D3.4 N-14 登记的验收命令集之一)实测 FAIL:
  我方 docs/46 docs/47(2026-09-20 建)与投顾组 docs/46-投顾Agent需求文档.md
  docs/47-投顾Agent功能架构文档.md(2026-09-16 建)同号。
- 按「后到者让位」改名:docs/48-可改文件白名单.md / docs/49-底座会签申请单-2026-09-19.md,
  同步 8 处引用。修复后:checked 54 documents, no number collision,exit 0。

三、前端品牌残留(W12 合并静默回退)
- employee-advisor/dashboard/index.html 与 customer/advisor-plans/index.html 的
  <title> 仍是 南方财富(投顾组分支带回)→ 按 DEC-27 改为 南方基金。
- HTTP 实测两页标题已正确;全仓 app/ 复查 南方财富 = 0。

四、文档口径校准(12 处事实漂移)
- D1.1:D2.1 版本 v5.3 → v6.26(§4.0 / §4.1 / §1 / §2 四处长期错误);
  §8 四行遗留项闭合(D-5 / D-6 / D-7 / 仓库副本同步)+ 新增 §22 §23 留痕;
  §10.2「本区不在任何 git 仓库内」更正为已入库;新增两编号 ⇒ 计数 56 → 58 全量同步。
- D2.2:顶栏徽标 v2.4 与元数据 v2.5 自相矛盾 → 统一;「投顾已清除」→ 状态更新
  (模块 2026-09-20 已恢复,但客服范围裁定 §1.7 / RK-10 不变)。
- D2.3:徽标 v1.0 · 7 批次 51 项 → v1.1 · 8 批次 57 项;投顾清除后果 + §7.1 头号风险
  + 风险表 + 不触碰行全部加恢复口径。
- D2.4:v1.3 变更说明 ⑦ / §1.4 Out of scope / Q-09 加投顾恢复口径。
- D2.5:advisor_t 自相矛盾口径改写为账号表一行 + 口径更正;五项自检首选改为
  一键脚本 启动演示.bat / demo.ps1;补 D3.8 与未发布 advisor:* 白名单登记。
- D2.6:门禁数字 1856/2 → 1909/3 skipped、ruff 19 → 20、补 portal_api_check 行;
  §10 两项已闭环(密钥轮换已工具化、A-10 组 3/4 已补签);头部加 W12/W13 状态更新。
- D4.5:顶部状态更新补指向 D4.7。
- 新增 开发文档/D4.7-投顾模块恢复记录-2026-09-20.md(CS-PURGE-2026-014):
  时间线、8 项恢复动作、客服线不变的结论、DEC-19 理由更正、遗留 1 项、失误登记。
- _consistency.py(维护侧):§三 改为「投顾状态口径检查」,合法语境扩为
  清除史 / 恢复史 / 不属本 Agent 范围。

五、回归实测(全绿)
- pytest -q:1909 passed / 3 skipped / 0 failed
- ruff check app tools tests:20(与 W12 持平,未引入新债)
- mypy app:2(= 既有基线)
- tools/check_authoritative_docs.py:54 文档无编号冲突(exit 0)
- tools/e2e_smoke_test.py --read-only:31/31
- tools/portal_api_check.py:40 项 通过 35 / 失败 0 / 跳过 5
- _eval_harness/http_probe.py:11/11 succeeded
- _consistency.py:GATE PASS
- demo.ps1 -SkipStart -NoBrowser:五项自检全过、退出码 0
- 权威副本 ↔ 仓库:逐字节一致(客服agent 24 / 开发文档 52)

六、未做(如实登记)
- 投顾 config_release 工具白名单(advisor:*)仍未发布 ⇒ 投顾 Agent 工具调用 fail closed
  (实测 active_agent_tools 仅 customer_service:* 4 项 + risk:* 4 项)。与客服线无关;
  要演投顾线先跑 tools/publish_advisor_demo_config.py --apply。
- 两把 key 的实际轮换需你在控制台建新 key(无法代做),流程见 D3.8。
2026-09-20 15:27:07 +08:00

10 KiB
Raw Permalink Blame History

D3.8 · 模型密钥轮换与凭据安全操作手册

体系编号:D3.8 · 域:三、现行权威·完整版与专项 · 编号体系见 D1.1 §4.0

编号:CS-OPS-2026-023 | 版本:v1.0 | 日期:2026-09-20 | 状态:现行(操作手册,答辩后当轮执行) 性质:操作手册(SOP) —— 回答「两把模型密钥已经在会话/文档里出现过明文,怎么安全地换掉、怎么证明换对了」。 配套:工具 group_fqcd_jr\tools\rotate_api_keys.py(本手册的唯一执行入口);演示脚本 D2.5;答辩报告 D2.6 §10 第 4 项;会话留痕 D1.6 §4.35 第五节第 2 项与 §4.39。 🔴 铁律:任何文档、任何提交、任何截图都不落 key 值(含掩码全量、含片段)。本手册只写变量名与校验方法。


0. 一句话结论

控制台建新 key(人工,不可自动化)→ 跑一个脚本改 .env → 重启 API + Worker → 复核 → 再吊销旧 key。 核心风险不是「会不会改错一个变量」,而是同一个 key 在 .env 里有多个变量名:漏改一个,就会出现「入库成功但检索失败」这类没有任何报错的症状。


1. 为什么要轮换(触发条件)

# 事实
1 本项目的两把模型密钥(DashScope/Qwen 一把、DeepSeek 一把)在对话过程中出现过明文(用户于 2026-09-18 直接粘贴在会话里)
2 会话记录(D1.6 等)与全部文档从未写入 key 值(W12 入库前已做过全仓密钥扫描:真实密钥只出现在被 .gitignore 命中的 .env)
3 但「明文已在会话里出现过」本身就是凭据泄露事件 ⇒ 按泄露处置:轮换 + 吊销旧 key

轮换不等于改一行:改完之后系统必须仍然能工作,这要用第 4 步的复核来证明,而不是靠「密码改成功了」的直觉。


2. 轮换前的现状(2026-09-20 实测,只写变量名与长度)

2.1 .env 里的 5 个变量(group_fqcd_jr\.env,已被 .gitignore 命中,不入库)

变量名 实测长度 谁在读它(代码取证)
QWEN_API_KEY 115 🔴 model_endpoint_config.id=1 的 secret_ref = env:QWEN_API_KEY(检索侧向量化);tools\load_knowledge_milvus.py:50(灌库侧)
QWEN_EMBEDDING_API_KEY 115 tools\configure_embedding_endpoint.py:92(仅在端点不存在时用于创建端点)
DASHSCOPE_API_KEY 115 app\service\promotion_image_service.py:28(推广素材配图,缺则降级不报错)
DEEPSEEK_API_KEY 35 🔴 model_endpoint_config.id=2 的 secret_ref = env:DEEPSEEK_API_KEY(生成/判定);app\service\advisor_reason_service.py:145
OFFSITE_DEEPSEEK_API_KEY 35 app\service\offsite_document_recognition_adapter.py:610(场外文档识别)

📌 脚本侧另有一个变量名 DEEPSEEK_API_KEY_NL2SQL(nl2sql_yc.py:156 优先取它、缺省回退 DEEPSEEK_API_KEY)。当前 .env 未定义该变量 ⇒ 走回退,无需处理。

2.2 数据库侧:secret_ref 存的是变量名,不是值

id endpoint_code provider model_name secret_ref status
1 knowledge-embedding-qwen-v3 qwen text-embedding-v3 env:QWEN_API_KEY active
2 deepseek-flash deepseek deepseek-chat env:DEEPSEEK_API_KEY active

⇒ 轮换只改 .env,不需要动 DB。(EnvironmentSecretResolver 在运行时按名字去环境变量取值。)

2.3 键值分组(这就是脚本存在的理由)

组 变量 必须满足
Qwen(DashScope) QWEN_API_KEY / QWEN_EMBEDDING_API_KEY / DASHSCOPE_API_KEY 三个 = 同一个值(同源同一把千问 key)
DeepSeek DEEPSEEK_API_KEY / OFFSITE_DEEPSEEK_API_KEY 两个 = 同一个值(场外识别复用同一把)

3. 五步流程

第 1 步 · 控制台建新 key(人工,不可自动化)

服务 入口 建什么
阿里云 DashScope(百炼) 控制台 → API-KEY 管理 新建一把 Qwen key(形如 sk-…,实测长度 115)
DeepSeek 开放平台 控制台 → API keys 新建一把 DeepSeek key(形如 sk-…,实测长度 35)

⚠️ 先不要吊销旧 key —— 旧的留作回退,直到第 4 步复核通过。

第 2 步 · 体检(不改任何文件,先看现状)

cd D:\桌面\金融\group_fqcd_jr
$env:PYTHONPATH='D:\桌面\金融\group_fqcd_jr'
& '.\.venv\Scripts\python.exe' tools\rotate_api_keys.py --check

期望输出(2026-09-20 实测即为此):

· Qwen(DashScope):
    QWEN_API_KEY               sk-ws-…(115 位)
    QWEN_EMBEDDING_API_KEY     sk-ws-…(115 位)
    DASHSCOPE_API_KEY          sk-ws-…(115 位)
    ✅ 同值且非空
· DeepSeek:
    DEEPSEEK_API_KEY           sk-e35…(35 位)
    OFFSITE_DEEPSEEK_API_KEY   sk-e35…(35 位)
    ✅ 同值且非空

出现 ❌ 有变量为空 或 ❌ 取值不一致 时先别轮换,先查清为什么(不一致本身就是缺陷)。

第 3 步 · 轮换(交互式,输入不回显)

& '.\.venv\Scripts\python.exe' tools\rotate_api_keys.py

脚本会:① 提示输入新的 Qwen key(getpass,不回显);② 提示输入新的 DeepSeek key;③ 校验格式(sk- 前缀 + 长度下限),不通过则整体不写入;④ 备份 .env.bak-<时间戳>(已被 .gitignore 命中);⑤ 把三个 Qwen 变量写成同一个值、两个 DeepSeek 变量写成同一个值;⑥ 只替换目标行,其余行(含注释、缩进、顺序)原样保留。

第 4 步 · 重启 + 复核(不做这步等于没换)

# 1) 先停 API 与 Worker(进程里还是旧 key)
Get-CimInstance Win32_Process -Filter "Name like '%python%'" | Where-Object { $_.CommandLine -match 'uvicorn app.main:app|app.worker' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
# 2) 再起(日志见 _http_api.log / _http_worker.log)
Start-Process -FilePath 'D:\桌面\金融\group_fqcd_jr\.venv\Scripts\python.exe' -ArgumentList '-m','uvicorn','app.main:app','--host','127.0.0.1','--port','8000','--log-level','warning' -WorkingDirectory 'D:\桌面\金融\group_fqcd_jr' -WindowStyle Hidden
Start-Process -FilePath 'D:\桌面\金融\group_fqcd_jr\.venv\Scripts\python.exe' -ArgumentList '-m','app.worker' -WorkingDirectory 'D:\桌面\金融\group_fqcd_jr' -WindowStyle Hidden
# 复核项 命令 / 判据
1 服务就绪 GET /internal/health/ready → {"status":"ready", ...}
2 全链路探针 _eval_harness\http_probe.py → 11/11 succeeded(含访客线 + 客户线 + 反诈/账户/代办/画像/费率/多轮)
3 一键自检 demo.ps1 -SkipStart -NoBrowser → 五项自检全过(其中第 2 项「向量库三集合可查」即检索侧向量化的真实证明)
4 行情链路 tools\sync_market_prices.py → 跑通(另证明外部 HTTP 出口正常)
5 检索侧单独验(可选) 发一句知识型问句(如「七日年化是什么意思」)必须走 E3 正常作答;若变成「答不上来」或转人工,就是 key 没配对

第 5 步 · 吊销旧 key(只在第 4 步全过之后)

回到两个控制台,把旧 key 删除/禁用。删除后建议再跑一次第 4 步的第 2 项(证明系统确实已不再依赖旧 key)。


4. 三个必须知道的坑

# 坑 症状 为什么
1 只改了 QWEN_API_KEY,漏改另两个 「入库成功 / 检索失败」,或旧 key 吊销后才炸 三个变量来自同一把 key 的三个变量名,被三处不同代码读取(§2.1)
2 只改了 DEEPSEEK_API_KEY,漏改 OFFSITE_DEEPSEEK_API_KEY 主链路正常,场外文档识别静默降级 场外适配器读的是另一个变量名(§2.1)
3 手改 .env 时带了 BOM / 改了编码 / 改了换行 第一个变量读不出来(后面的都正常)⇒ 症状诡异、难查 .env 是纯文本按行解析;脚本改写时保留原编码与原换行,就是为了防这一条

📌 补充坑:改完不重启。UVicorn / Worker 进程内的环境变量是启动时读入的,.env 改了但进程没重启 ⇒ 仍然用旧 key。


5. 回退方式

场景 怎么做
第 3 步写入后想撤回 用同目录的 .env.bak-<时间戳> 原样覆盖回去,然后重启 API + Worker
新 key 在控制台建错了 / 想作废 在控制台禁用新 key,再用备份回退
旧 key 已吊销、新 key 又不能用 只能回控制台再建一把 —— 所以第 5 步必须排在第 4 步之后

6. 与其它文档的关系(避免两处口径漂移)

文档 关系
D2.6 §10 第 4 项「两把 API key 轮换」 本手册是它的执行细则;该项状态已从「手工 4 步」改为「跑一个脚本(D3.8)」
D2.5 §1 五项自检 第 4 步复核复用其中的「向量库三集合可查」
D1.6 §4.39 本轮(W13)新增工具与本手册的会话留痕
group_fqcd_jr\docs\26-JWT密钥管理与轮换.md 讲的是 JWT 签名密钥,与本手册的模型 API key 是两回事(前者是自签自验,后者是外部服务凭据)

7. 如实声明(本手册的能力边界)

项 说明
不能自动化第 1 步 / 第 5 步 控制台建 key 与吊销 key 必须由你本人在浏览器操作(无 API 授权、也不应为此开授权)
本手册不记录任何 key 值 连掩码全量都不落 —— 掩码+长度对穷举无用,但片段会缩小搜索空间,故一并省略
--check 只能证明「一致且非空」 它不联网验证 key 是否有效;key 是否可用由第 4 步的真实链路证明
未覆盖 阿里云/DeepSeek 控制台的子账号与权限模型(本手册只处理主账号 key 轮换);密钥托管系统(KMS/Vault)接入 —— 当前项目用 .env,未上密钥托管