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。
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
# 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 步 · 体检(不改任何文件,先看现状)
|
||||
|
||||
```powershell
|
||||
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 步 · 轮换(交互式,输入不回显)
|
||||
|
||||
```powershell
|
||||
& '.\.venv\Scripts\python.exe' tools\rotate_api_keys.py
|
||||
```
|
||||
|
||||
脚本会:① 提示输入新的 Qwen key(`getpass`,**不回显**);② 提示输入新的 DeepSeek key;③ 校验格式(`sk-` 前缀 + 长度下限),**不通过则整体不写入**;④ 备份 `.env.bak-<时间戳>`(已被 `.gitignore` 命中);⑤ 把**三个 Qwen 变量写成同一个值、两个 DeepSeek 变量写成同一个值**;⑥ 只替换目标行,其余行(含注释、缩进、顺序)**原样保留**。
|
||||
|
||||
### 第 4 步 · 重启 + 复核(**不做这步等于没换**)
|
||||
|
||||
```powershell
|
||||
# 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`,未上密钥托管 |
|
||||
Reference in New Issue
Block a user