171 lines
10 KiB
Markdown
171 lines
10 KiB
Markdown
# 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`,未上密钥托管 |
|