Files
hms/docs/superpowers/specs/2026-04-26-pii-encryption-expansion-design.md
iven 1265935fa3 chore: 设计规格文档 + 销售数据 + 脚本工具 + 根目录 monorepo 配置
- docs/: 设计规格、讨论记录、销售数据、健康管理文档
- scripts/: 辅助脚本
- package.json + pnpm-lock.yaml: monorepo 根配置
2026-04-28 00:20:37 +08:00

592 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PII 分级加密扩展设计
> 日期: 2026-04-26 | 状态: 草案 | 作者: iven + Claude
## 目录
1. [背景与动机](#1-背景与动机)
2. [设计目标](#2-设计目标)
3. [当前状态分析](#3-当前状态分析)
4. [分级加密策略](#4-分级加密策略)
5. [加密基础设施提升 (erp-core)](#5-加密基础设施提升)
6. [每租户独立密钥管理](#6-每租户独立密钥管理)
7. [Entity 层改造](#7-entity-层改造)
8. [Service 层集成](#8-service-层集成)
9. [数据迁移](#9-数据迁移)
10. [边界场景处理](#10-边界场景处理)
11. [分步实施计划](#11-分步实施计划)
12. [测试策略](#12-测试策略)
13. [风险与缓解](#13-风险与缓解)
---
## 1. 背景与动机
HMS 健康管理平台当前仅加密了 `patient.id_number`(身份证号)一个字段,使用 AES-256-GCM + HMAC-SHA256 搜索索引。其余 30+ 个 PII 字段(患者姓名、电话、过敏史、病史摘要、咨询内容等)均为明文存储。
**驱动力:**
- 体检中心/医疗机构在采购前会进行安全审计,明文 PII 是 dealbreaker
- 《个人信息保护法》对医疗健康信息有特殊保护要求
- 多租户 SaaS 架构下,数据隔离需要从访问控制扩展到存储加密
- 三专家评审一致指出 PII 加密范围不足是高优合规风险
**约束:**
- 系统前期不涉及实际医疗行为PII 加密是合规门槛而非实时临床安全
- 保持可搜索性:患者姓名是高频搜索字段,不能因加密丧失搜索能力
- 性能可接受:加密/解密开销不能显著影响 API 响应时间
---
## 2. 设计目标
| 目标 | 指标 |
|------|------|
| PII 字段加密覆盖 | 从 1 个字段扩展到 15+ 个高风险字段 |
| 多租户密钥隔离 | 每租户独立 DEK泄漏不影响其他租户 |
| 搜索能力保持 | HMAC 精确搜索 + 姓名明文模糊搜索 |
| 性能影响 | 单条解密 < 0.1ms,列表页不触发解密 |
| 零停机迁移 | 现有明文数据逐步加密,不锁表 |
---
## 3. 当前状态分析
### 已有加密能力
| 组件 | 位置 | 状态 |
|------|------|------|
| `HealthCrypto` | `crates/erp-health/src/crypto.rs` | AES-256-GCM + HMAC-SHA256 |
| 身份证号加密 | `patient_service.rs` | 已上线 |
| 脱敏管道 | `crates/erp-health/src/service/masking.rs` | `mask_id_number`, `mask_phone` |
| AI 脱敏层 | `crates/erp-ai/src/sanitization/mod.rs` | DTO 去标识化 |
| 密钥配置 | `crates/erp-server/config/default.toml` | 环境变量注入 |
### 明文高风险字段清单
**患者 (patient) — 8 个未加密 PII 字段:**
- `name` — 姓名(决定不加密,保持搜索性)
- `emergency_contact_phone` — 紧急联系人电话
- `allergy_history` — 过敏史
- `medical_history_summary` — 病史摘要
- `emergency_contact_name` — 紧急联系人姓名
- `birth_date` — 出生日期
- `blood_type` — 血型
- `notes` — 备注
**家庭成员 (patient_family_member) — 2 个:**
- `phone` — 电话
- `name` — 姓名
**咨询消息 (consultation_message) — 1 个:**
- `content` — 咨询/问诊内容
**随访记录 (follow_up_record) — 3 个:**
- `result` — 随访结果
- `patient_condition` — 患者状况
- `medical_advice` — 医嘱
**诊断 (diagnosis) — 1 个:**
- `notes` — 诊断备注
**医生档案 (doctor_profile) — 1 个:**
- `license_number` — 执业证号
**透析记录 (dialysis_record) — 2 个:**
- `symptoms` — 症状 JSON
- `complication_notes` — 并发症记录
**化验报告 (lab_report) — 1 个:**
- `doctor_notes` — 医生备注
---
## 4. 分级加密策略
### Tier 定义
| 级别 | 存储方式 | 搜索方式 | 展示方式 | 适用字段 |
|------|---------|---------|---------|---------|
| **Tier 1** | AES-256-GCM 加密 | HMAC-SHA256 精确匹配 | 解密后展示/脱敏 | 高敏感、可枚举的字段 |
| **Tier 2** | 明文 | 直接 SQL LIKE/模糊 | API 脱敏展示 | 中敏感、高频搜索的字段 |
| **Tier 3** | 明文 | 直接 SQL | 原样展示 | 非敏感或非 PII 字段 |
### Tier 1 — 加密存储字段
| Entity | 字段 | HMAC 搜索 |
|--------|------|----------|
| patient | `emergency_contact_phone` | `emergency_contact_phone_hash` |
| patient | `allergy_history` | — |
| patient | `medical_history_summary` | — |
| patient_family_member | `phone` | `phone_hash` |
| consultation_message | `content` | — |
**咨询内容搜索说明:** `consultation_message.content` 加密后无法做全文搜索。替代方案按咨询会话consultation维度搜索和过滤通过状态、时间、医生等非加密字段进入详情后查看具体消息内容。如未来需要全文搜索可引入加密索引如 Order-Preserving Encryption 或专用密文搜索引擎),但 V1 不实施。
| follow_up_record | `result` | — |
| follow_up_record | `patient_condition` | — |
| follow_up_record | `medical_advice` | — |
| doctor_profile | `license_number` | `license_number_hash` |
| diagnosis | `notes` | — |
| dialysis_record | `complication_notes` | — |
| dialysis_record | `symptoms` (JSON) | — |
| lab_report | `doctor_notes` | — |
**JSON 字段加密说明:** `dialysis_record.symptoms` 类型为 `serde_json::Value`,加密时先序列化为 JSON 字符串再加密,解密后反序列化回 `Value`
**注:** `patient.id_number``id_number_hash` 已存在,无需改动。
### Tier 2 — 明文 + API 脱敏
| Entity | 字段 | 脱敏规则 |
|--------|------|---------|
| patient | `emergency_contact_name` | 保留首字 + `**`(如 `张**`),两字姓名也适用(`张*` |
| patient | `birth_date` | 仅显示年份 |
| patient_family_member | `name` | 保留首字 + `**`,同上 |
### Tier 3 — 明文
`name`, `gender`, `blood_type`, 数值型体征数据, 日期, 状态字段, `icd_code`, `diagnosis_name`, `notes`patient 和 health_record 的备注字段,内容不可预测且非临床核心)等。
**分类澄清:** `notes` 字段在不同 Entity 中分级不同。`patient.notes` 归入 Tier 3一般备注`diagnosis.notes` 归入 Tier 1临床诊断备注`follow_up_record``result`/`patient_condition`/`medical_advice` 归入 Tier 1。具体分级以上面 Tier 1/2/3 表格为准。
**设计决策:患者姓名不加密。** 原因:医疗场景下姓名不是真正意义上的隐私(前台叫号、医生喊名),保持明文可搜索对运营效率至关重要。加密姓名会强制改变搜索交互模式(从"先搜索"变成"先选择"),代价过大。
---
## 5. 加密基础设施提升
### 5.1 从 HealthCrypto 到 PiiCrypto
`crates/erp-health/src/crypto.rs` 中的 `HealthCrypto` 提升到 `crates/erp-core/src/crypto/`
```
crates/erp-core/src/crypto/
├── mod.rs # PiiCrypto 公开接口
├── engine.rs # AES-256-GCM 加密/解密引擎
├── hmac.rs # HMAC-SHA256 搜索索引
├── key_manager.rs # 每租户 DEK 管理
└── masking.rs # 脱敏管道(从 health/service/masking.rs 提升)
```
### 5.2 PiiCrypto 接口设计
```rust
pub struct PiiCrypto {
kek: [u8; 32], // Master KEK
dek_cache: DashMap<Uuid, CachedDek>, // 租户 DEK 缓存
}
struct CachedDek {
dek: [u8; 32],
version: u32,
loaded_at: Instant,
}
impl PiiCrypto {
/// 加密单个字段,返回 Base64(nonce + ciphertext + tag)
pub fn encrypt(&self, dek: &[u8; 32], plaintext: &str) -> Result<String>;
/// 解密单个字段
pub fn decrypt(&self, dek: &[u8; 32], ciphertext: &str) -> Result<String>;
/// 生成 HMAC-SHA256 搜索索引
pub fn hmac_hash(&self, dek: &[u8; 32], value: &str) -> String;
/// 批量解密(避免重复 DEK 加载)
pub fn decrypt_batch(&self, dek: &[u8; 32], ciphertexts: &[String]) -> Result<Vec<String>>;
/// 获取/创建指定租户的 DEK
pub async fn get_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result<DekWithVersion>;
/// 轮换指定租户的 DEK
pub async fn rotate_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result<()>;
}
```
### 5.3 依赖关系变化
```
Before: erp-health → (内部 crypto.rs)
After: erp-core (crypto/) ← erp-health (调用 PiiCrypto)
← 未来其他模块
```
### 5.4 Cargo.toml 依赖变更
`erp-core/Cargo.toml` 需要新增:
```toml
[dependencies]
aes-gcm = "0.10"
hmac = "0.12"
sha2 = "0.10"
base64 = "0.22"
hex = "0.4"
dashmap = "6"
```
`erp-health/Cargo.toml` 可移除 `aes-gcm`, `hmac`, `sha2`, `base64`, `hex`(已提升到 erp-core改为依赖 `erp-core` 的 crypto 模块。
### 5.5 erp-ai 依赖关系澄清
当前 erp-ai 通过 `HealthDataProvider` trait 获取**已脱敏**的 DTO不含 name/phone/id_number**不需要**直接访问加密数据。PiiCrypto 提升到 erp-core 后erp-ai 不需要调用 PiiCrypto 解密。AI 脱敏层继续作为独立防线运作。
如果未来 AI 需要原始 PII 数据(如精确匹配患者),可通过新增 `HealthDataProviderWithPii` trait 提供,此时才需要 erp-ai 调用 PiiCrypto。
---
## 6. 每租户独立密钥管理
### 6.1 密钥层级
```
Master KEK (Key Encryption Key)
来源: 环境变量 ERP__CRYPTO__KEK
用途: 加密保护所有租户的 DEK
格式: 32 字节 hex-encoded
└── Per-Tenant DEK (Data Encryption Key)
来源: 首次使用时随机生成,用 KEK 加密后存入 DB
用途: 加密该租户的 PII 数据
格式: 32 字节随机
```
### 6.2 数据库表
```sql
CREATE TABLE tenant_crypto_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
encrypted_dek VARCHAR(128) NOT NULL, -- AES-256-GCM 加密的 DEK
key_version INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID,
updated_by UUID,
deleted_at TIMESTAMPTZ,
version INTEGER NOT NULL DEFAULT 1,
UNIQUE(tenant_id, key_version)
);
-- 项目规范要求所有表包含: id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version
```
### 6.3 DEK 缓存策略
| 参数 | 值 | 原因 |
|------|-----|------|
| 最大缓存数 | 100 个租户 | 典型 SaaS 实例租户数 < 100 |
| TTL | 5 分钟 | 平衡安全性和性能 |
| 淘汰策略 | LRU | 简单有效 |
| 强制刷新 | 密钥轮换后 | 确保新密钥立即生效 |
使用 `DashMap`(并发安全 HashMap实现缓存避免每次请求查询数据库。
### 6.4 密钥轮换流程
1. 管理员调用 `POST /api/v1/admin/tenants/{id}/rotate-key`
2. 生成新 DEK用 KEK 加密,写入 `tenant_crypto_keys`version +1
3. 后台任务启动:
- 读取该租户所有加密数据
- 用旧 DEK 解密 → 用新 DEK 加密
- 更新 `key_version` 字段
- 分批处理(每批 100 条),每批提交一次
4. 轮换期间,读取侧根据每条记录的 `key_version` 字段精确选择对应 DEK不做"尝试解密 + 回退"(避免掩盖数据篡改或密钥管理错误)
5. 全部完成后,标记旧 DEK `is_active = false`
---
## 7. Entity 层改造
### 7.1 新增伴生字段
每个需要 HMAC 搜索的字段,在 SeaORM Entity 中新增 `_hash` 列:
| Entity | 新增字段 | 迁移操作 |
|--------|---------|---------|
| patient | `emergency_contact_phone_hash: Option<String>` | ALTER TABLE ADD COLUMN |
| patient_family_member | `phone_hash: Option<String>` | ALTER TABLE ADD COLUMN |
| doctor_profile | `license_number_hash: Option<String>` | ALTER TABLE ADD COLUMN |
**HMAC hash 列索引:** 每个 `_hash` 列创建普通 B-tree 索引(`CREATE INDEX idx_{table}_{field}_hash ON {table}({field}_hash) WHERE {field}_hash IS NOT NULL`),确保 HMAC 搜索不会退化为全表扫描。
### 7.2 Entity Model 变化示例
```rust
// crates/erp-health/src/entity/patient.rs
// 改造前:
pub struct Model {
pub id_number: Option<String>, // 已加密
pub id_number_hash: Option<String>, // 已有
pub emergency_contact_phone: Option<String>, // 明文
pub allergy_history: Option<String>, // 明文
// ...
}
// 改造后:
pub struct Model {
pub id_number: Option<String>, // 已加密 (不变)
pub id_number_hash: Option<String>, // 已有 (不变)
pub emergency_contact_phone: Option<String>, // 现在存储加密值
pub emergency_contact_phone_hash: Option<String>, // 新增: HMAC 搜索索引
pub allergy_history: Option<String>, // 现在存储加密值
// ...
}
```
**重要:** Entity 字段类型不变(仍为 `Option<String>`),只是存储内容从明文变为密文。这确保 SeaORM 查询层无需改动。
---
## 8. Service 层集成
### 8.1 加密/解密注入点
Service 层在每个 CRUD 操作中透明处理加密:
| 操作 | 加密处理 | 解密处理 |
|------|---------|---------|
| Create | 加密 Tier 1 字段 + 生成 HMAC | — |
| Update | 重新加密变更的 Tier 1 字段 + 更新 HMAC | — |
| Get by ID | — | 解密 Tier 1 字段 |
| List | — | 列表不展示加密字段(返回 None |
| Search | HMAC hash 替代明文搜索条件 | — |
### 8.2 PiiCrypto 注入方式
```rust
// erp-server/src/state.rs
pub struct HealthState {
pub pii_crypto: PiiCrypto, // 替换原来的 HealthCrypto
// ...
}
// patient_service.rs
pub async fn create_patient(
db: &DatabaseConnection,
crypto: &PiiCrypto,
tenant_id: Uuid,
input: CreatePatientRequest,
) -> Result<PatientResponse> {
let dek = crypto.get_dek(tenant_id, db).await?;
// 加密 Tier 1 字段
let encrypted_phone = input.emergency_contact_phone
.map(|p| crypto.encrypt(&dek.key, &p))
.transpose()?;
let phone_hash = input.emergency_contact_phone
.map(|p| crypto.hmac_hash(&dek.key, &p));
// ... 插入数据库
}
```
---
## 9. 数据迁移
### 9.1 迁移策略
采用**渐进式迁移**,不锁表,不中断服务:
1. **部署新代码** — 新代码根据 `key_version` 字段判断:`NULL` = 明文(直接返回),有值 = 密文(解密后返回)。不依赖 Base64 格式检测(自由文本字段本身可能是合法 Base64
2. **后台迁移任务** — 逐条加密现有明文数据
3. **完成切换** — 所有数据加密后,移除明文兼容路径
### 9.2 迁移任务设计
```rust
pub async fn migrate_pii_encryption(
db: &DatabaseConnection,
crypto: &PiiCrypto,
tenant_id: Uuid,
) -> Result<MigrationReport> {
let dek = crypto.get_dek(tenant_id, db).await?;
let batch_size = 100;
loop {
// 读取一批未迁移的记录key_version < current 或 key_version IS NULL
let batch = find_unencrypted_patients(db, tenant_id, batch_size).await?;
if batch.is_empty() { break; }
for patient in &batch {
// 加密明文字段
let encrypted_phone = patient.emergency_contact_phone.as_ref()
.map(|p| crypto.encrypt(&dek.key, p))
.transpose()?;
let phone_hash = patient.emergency_contact_phone.as_ref()
.map(|p| crypto.hmac_hash(&dek.key, p));
// 更新记录
update_patient_encrypted(db, patient.id, encrypted_phone, phone_hash, dek.version).await?;
}
sleep(Duration::from_millis(50)).await; // 避免压垮数据库
}
}
```
### 9.3 Entity 新增 key_version 字段
所有包含 Tier 1 字段的 Entity 新增 `key_version: Option<i32>` 列,用于:
- 跟踪每条记录使用的 DEK 版本
- 支持渐进式迁移和轮换
- NULL 表示未加密(明文)
### 9.4 回滚计划
部署后如果需要回滚到旧代码:
- **新创建的加密数据:** 旧代码无法读取(`key_version` 非空的记录)。回滚前需要运行"解密迁移"脚本,将加密数据还原为明文。
- **未迁移的明文数据:** 旧代码可正常读取。
- **回滚脚本:** 提供与加密迁移对称的解密迁移脚本,从最新 `key_version` 的 DEK 解密所有字段,清除 `key_version`(设为 NULL
- **回滚窗口:** 建议在 Phase A 部署后 24 小时内保持回滚能力,超过 24 小时确认无问题后可移除明文兼容路径。
---
## 10. 边界场景处理
### 10.1 列表页性能
**策略:** 列表页不返回 Tier 1 加密字段(设为 `None`),仅展示 Tier 2/3 字段。
这与当前 `model_to_resp` 的设计一致(列表中 `id_number` 已经返回 `None`),无需改变交互模式。
**前端影响评估:** 当前 `model_to_resp` 已经将 `emergency_contact_phone` 做脱敏处理(`mask_phone`)后返回。加密后,列表页的 `model_to_resp` 需要将该字段设为 `None`(因为不解密),详情页的 `model_to_resp_decrypted` 需要解密后再脱敏。前端列表页如果展示了 `emergency_contact_phone`,需要适配为"详情中查看"。`patient_family_member``doctor_profile` 同理。
### 10.2 AI 分析管道
erp-ai **不直接调用 PiiCrypto**。它通过 `HealthDataProvider` trait在 erp-core 中)获取已脱敏的 DTO不含 name/phone/id_number 等原始 PII。PiiCrypto 对 erp-ai 完全透明——erp-health 在提供数据给 erp-ai 之前已经在 Service 层完成了解密+脱敏。AI 脱敏层(`SanitizationService`)作为二次防护,确保发送给 LLM 的数据不包含原始 PII。
### 10.3 数据导出
导出操作需要独立的权限码(如 `patient.export`Service 层解密后输出。审计日志记录所有导出操作。
### 10.4 小程序端
小程序不直接处理加密数据。所有加密/解密在服务端完成API 响应中的 Tier 2 字段做脱敏处理。
### 10.5 跨租户数据泄漏
即使数据库层面发生泄漏(如 SQL 注入),攻击者没有 KEK 就无法解密 DEK没有 DEK 就无法解密 PII 数据。HMAC hash 字段不可逆,不泄漏原始值。
---
## 11. 分步实施计划
### Phase A — 基础设施 + Patient 实体2-3 天)
**目标:** 加密基础设施落地patient 实体全面加密。
| 步骤 | 内容 | 预估 |
|------|------|------|
| A1 | 提升加密基础设施到 erp-core/src/crypto/ | 0.5 天 |
| A2 | 实现 `tenant_crypto_keys` 表 + DEK 管理 | 0.5 天 |
| A3 | 修改 patient Entity新增 hash 字段 + key_version | 0.5 天 |
| A4 | 修改 patient_service 集成 PiiCrypto | 0.5 天 |
| A5 | 数据迁移脚本patient 表现有数据) | 0.5 天 |
| A6 | 单元测试 + 集成测试 | 0.5 天 |
**验证标准:**
- `cargo check` + `cargo test` 全部通过
- 创建新患者 → 数据库中 phone/allergy 为密文hash 正确
- 查询患者详情 → API 返回解密后的明文
- 搜索电话号码 → 通过 HMAC hash 匹配
### Phase B — 全 Entity 扩展 + 密钥轮换2-3 天)
**目标:** 所有 Tier 1 字段加密,管理端支持密钥轮换。
| 步骤 | 内容 | 预估 |
|------|------|------|
| B1 | consultation_message, follow_up_record 加密 | 0.5 天 |
| B2 | patient_family_member, doctor_profile 加密 | 0.5 天 |
| B3 | dialysis_record, lab_report 加密 | 0.5 天 |
| B4 | 密钥轮换管理端点 + 后台任务 | 0.5 天 |
| B5 | DEK 缓存DashMap LRU | 0.5 天 |
| B6 | 全量集成测试 + 性能基准 | 0.5 天 |
**验证标准:**
- 所有 Tier 1 字段在数据库中为密文
- 所有现有 API 功能正常(透明加密/解密)
- 密钥轮换后数据可正常读写
- 批量解密 50 条记录 < 10ms
---
## 12. 测试策略
### 12.1 单元测试
| 测试类别 | 覆盖内容 | 数量 |
|----------|---------|------|
| 加密引擎 | encrypt/decrypt 正确性、随机 nonce 不可预测 | 5 |
| HMAC 索引 | 相同输入相同输出、不同输入不同输出 | 3 |
| 密钥管理 | DEK 生成/缓存/轮换 | 5 |
| 脱敏管道 | 各脱敏规则正确性 | 4 |
### 12.2 集成测试
| 测试类别 | 覆盖内容 | 数量 |
|----------|---------|------|
| CRUD 加密流 | 创建→加密存储→查询→解密返回 | 8 |
| HMAC 搜索 | 精确匹配搜索正确性 | 3 |
| 多租户隔离 | 租户 A 的 DEK 无法解密租户 B 的数据 | 2 |
| 密钥轮换 | 轮换前后数据可正常访问 | 2 |
### 12.3 性能测试
| 指标 | 基准 | 目标 |
|------|------|------|
| 单字段加密 | — | < 0.05ms |
| 单字段解密 | — | < 0.05ms |
| 50 条批量解密 | — | < 10ms |
| HMAC 生成 | — | < 0.01ms |
---
## 13. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 加密后搜索性能下降 | 低 | 中 | HMAC 索引 + B-tree 索引 |
| 迁移过程中服务中断 | 低 | 高 | 渐进式迁移 + 兼容读取 |
| DEK 缓存一致性 | 中 | 中 | TTL + 轮换后强制刷新 |
| 密钥泄漏 | 低 | 极高 | KEK 不存数据库 + 环境变量注入 + 泄漏恢复流程(见下) |
| 加密字段误漏 | 中 | 中 | 代码审查 + 单元测试覆盖 |
| KEK 泄漏灾难恢复 | 极低 | 极高 | 见 §14 灾难恢复 |
---
## 14. KEK 泄漏灾难恢复
**场景:** Master KEK 泄露(如环境变量配置文件意外提交到 Git
**恢复流程:**
1. **立即轮换 KEK** — 生成新 KEK更新环境变量
2. **重新包装所有 DEK** — 用新 KEK 解密并重新加密每个租户的 DEK
```sql
-- 不需要重新加密 PII 数据,只需重新加密 DEK
UPDATE tenant_crypto_keys SET encrypted_dek = new_kek_encrypt(dek) WHERE is_active = true;
```
3. **清除 DEK 缓存** — 强制所有实例从数据库重新加载
4. **轮换泄漏渠道** — 轮换所有可能暴露 KEK 的凭据Git tokens、CI secrets、部署脚本
5. **审计追溯** — 检查访问日志,确认是否有未授权的数据访问
**关键优势:** KEK 泄漏只需要重新包装 DEKN 次轻量操作),不需要重新加密所有 PII 数据(可能数百万条记录)。这是 KEK/DEK 两层架构的核心价值。
**预防措施:**
- KEK 通过 secrets manager 注入,不写入文件系统
- CI/CD 管道中扫描 KEK 值,防止意外提交
- `.gitignore` 排除 `.env` 文件
---
## 附录:决策记录
| 决策 | 选项 | 选择 | 原因 |
|------|------|------|------|
| 加密范围 | 全面/分级/TDE | 分级 | 成本与收益平衡 |
| 姓名是否加密 | 加密/不加密 | 不加密 | 医疗场景下姓名不属隐私,搜索性更重要 |
| 基础设施位置 | health/core/独立 crate | erp-core | 通用性 + 简洁性平衡 |
| 密钥策略 | 全局/per-tenant/渐进 | 每租户独立 | 安全隔离,泄漏影响面最小 |
| 实施节奏 | 一次完成/分步/最小可用 | 分步 (A+B) | 风险分散,每步可验证 |