主题
10 - 安全体系:五层防护与全链路审计
一句话:安全按五层叠加且互为兜底,网关剥头换身份、实例用 Runtime Token 覆盖含静态资源的全部路径、NetworkPolicy 默认拒绝跨实例与内网访问、Vault 按租户派生密钥并配 Sealed Secrets 与 WASM 沙箱、审计靠 trace_id 串联;代价是每层只在配置不失配时有效,出站从全网 443 收紧为域名白名单后每加一个外部依赖都要改策略,Skill 进沙箱后也只能经受限 host 接口取数。
一、安全模型总览
数字分身产品的安全体系分为五层,每层独立生效、互为兜底:
二、各层详细说明
2.1 第一层:统一网关
职责:企业身份认证 + 流量入口管控
| 机制 | 说明 |
|---|---|
| SSO 认证 | OIDC/JWKS 验签,提取 user_id。客户端只带 Bearer,网关换发内部身份 |
| Header 清洗 | 剥离客户端伪造的 x-qwenpaw-runtime-token、x-sso-user 等 header |
| RT 注入 | ext_authz 服务从 Vault 缓存取 RT 注入请求头(详见第 03 章) |
| 角色拦截 | employee 角色不可达管理端点(/api/providers、/api/admin 等) |
| 限流 | Redis 滑动窗口 per-user 限流,按订阅档位分级 |
安全保证:员工不接触实例 RT,不存在"平台伪装登录"的可能。
2.2 第二层:Runtime 边界
职责:实例侧最后一道门,保护全部路径
QwenPaw 实例内置 RuntimeBoundaryMiddleware,对所有 HTTP 和 WebSocket 请求校验 x-qwenpaw-runtime-token header:
校验逻辑:
请求 header 中的 x-qwenpaw-runtime-token
vs
环境变量 QWENPAW_RUNTIME_INTERNAL_TOKEN(实例启动时注入)
使用 hmac.compare_digest 做时序安全比较(防 timing attack)
匹配 → 放行
不匹配 / 缺失 → 401(HTTP)/ 4401(WebSocket)保护范围:全部路径,包括 /api/healthz、/api/console/chat、/api/auth/*、首页、静态资源。没有任何路径绕过 RT 校验。
四通道验证矩阵:
| 场景 | 结果 |
|---|---|
| 无 RT header | 401 |
| 错误的 RT | 401 |
| 仅 Bearer token(无 RT) | 401 |
| 正确的 RT + Bearer token | 200 |
2.3 第三层:网络隔离
职责:K8s 网络层面的访问控制
yaml
# NetworkPolicy:实例间默认拒绝
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: instance-isolation
namespace: qwenpaw-instances
spec:
podSelector:
matchLabels:
app: qwenpaw
policyTypes:
- Ingress
- Egress
ingress:
# 仅允许网关和控制面访问实例端口
- from:
- namespaceSelector:
matchLabels:
name: qwenpaw-gateway
- namespaceSelector:
matchLabels:
name: qwenpaw-control
ports:
- port: 8088
protocol: TCP
# 允许 Prometheus 采集指标
- from:
- namespaceSelector:
matchLabels:
name: qwenpaw-infra
ports:
- port: 8088
protocol: TCP
egress:
# 允许访问模型 API
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # 禁止访问内网(除白名单)
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 443
# 允许访问 Nacos(服务发现)
- to:
- namespaceSelector:
matchLabels:
name: qwenpaw-infra
ports:
- port: 8848
# 允许访问 MinIO(备份/产物)
- to:
- namespaceSelector:
matchLabels:
name: qwenpaw-infra
ports:
- port: 9000
# 允许 DNS 查询
- to:
- namespaceSelector: {}
ports:
- port: 53
protocol: UDP关键约束:
- 实例间完全隔离:A 员工的实例无法访问 B 员工的实例
- 实例出站白名单控制:只能访问模型 API(443)、Nacos、MinIO,不能访问其他内网服务
- 仅网关、控制面、监控可访问实例端口
2.4 第四层:密钥治理
职责:全生命周期密钥管理
| 密钥类型 | 生成 | 存储 | 使用 | 轮换 | 吊销 |
|---|---|---|---|---|---|
| RT Token | token_urlsafe(48) | Vault(per-tenant Fernet 加密)+ Redis 缓存 | 网关注入 header | 90 天自动轮换 | 即时生效 |
| 模型 API Key | 管理员配置 | Vault + Sealed Secrets → secrets 卷 | 实例进程读取 | 按 Provider 策略 | 控制面替换 |
| MCP 凭证 | 管理员配置 | Vault → 控制面注入 header | MCP Client 连接 | 90 天自动轮换 | 即时生效 |
| IM 凭证 | 管理员配置 | Vault → 聚合器运行时 | 渠道聚合器 | 按平台策略 | 即时生效 |
Per-Tenant 密钥派生(P0 修复)
P0 修复:原方案所有租户使用同一个 Fernet 主密钥,一旦泄露全部租户凭证暴露。改为 per-tenant 密钥派生:每个租户使用独立的加密密钥,由主密钥 + tenant_id 通过 HKDF 派生。
python
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.fernet import Fernet
import base64
MASTER_KEY = os.environ["VAULT_MASTER_KEY"].encode() # 32 字节主密钥
def derive_tenant_key(tenant_id: str) -> bytes:
"""从主密钥派生租户专属密钥"""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"qwenpaw-vault-v1",
info=tenant_id.encode(),
)
derived_key = base64.urlsafe_b64encode(hkdf.derive(MASTER_KEY))
return derived_key
def encrypt_credential(tenant_id: str, plaintext: str) -> str:
"""使用租户专属密钥加密"""
key = derive_tenant_key(tenant_id)
f = Fernet(key)
return f.encrypt(plaintext.encode()).decode()
def decrypt_credential(tenant_id: str, ciphertext: str) -> str:
"""使用租户专属密钥解密"""
key = derive_tenant_key(tenant_id)
f = Fernet(key)
return f.decrypt(ciphertext.encode()).decode()安全效果:即使攻击者获取了某个租户的加密密钥,也无法解密其他租户的凭证。主密钥泄露时,只需轮换主密钥并重新派生所有租户密钥。
Sealed Secrets 保护 K8s Secret(P0 修复)
P0 修复:原方案 K8s Secret 默认 base64 编码,有 kubectl 读权限的运维人员可直接获取明文。改为 Sealed Secrets:Secret 在客户端使用集群公钥加密,只有集群内的 Sealed Secrets Controller 持有私钥可解密。运维人员即使有 K8s 读权限,也只能看到密文。
K8s RBAC 配套策略:
| 角色 | 权限 | 说明 |
|---|---|---|
| 运维人员 | pods/log 只读,secrets 无权限 | 可排查问题但无法获取密钥 |
| 开发人员 | pods/log 只读,secrets 无权限 | 同上 |
| Sealed Secrets Controller | sealedsecrets 读写,secrets 读写 | 唯一可解密的角色 |
| 控制面 ServiceAccount | secrets 指定 namespace 只读 | 仅可读取控制面自身 namespace 的 Secret |
禁止操作:
- 禁止
kubectl exec进入业务 Pod(通过 OPA/Gatekeeper 策略拦截) - 禁止
kubectl port-forward到业务 Pod(通过 NetworkPolicy 拦截) - 禁止
kubectl get secret读取实例 namespace 的 Secret(通过 RBAC 拦截)
Skill 代码沙箱执行(P0 修复)
P0 修复:原方案 Skill 代码在实例进程内无沙箱执行,恶意 Skill 可读取 secrets 卷、外传数据。改为 WASM 运行时沙箱:Skill 代码编译为 WASM 模块,在隔离的沙箱中执行,无法访问宿主文件系统和网络。
WASM 沙箱安全策略:
| 能力 | 原方案(无沙箱) | 新方案(WASM 沙箱) |
|---|---|---|
| 文件系统访问 | 可读写整个实例文件系统 | 不可访问,仅通过 host functions 传入/传出数据 |
| 网络访问 | 可发起任意 HTTP 请求 | 不可访问,需通过 host function 代理(受白名单控制) |
| 进程/线程 | 可 fork 进程 | 不可,单线程 WASM 执行 |
| 内存 | 共享实例进程内存 | 独立线性内存,不可越界 |
| 执行时间 | 无限制 | 可设置 gas 上限,超时自动终止 |
Skill 分发流程变更:
- Skill 包上传到企业市场时,自动编译 Python 代码为 WASM 模块
- 编译时进行静态安全分析(检测危险模式:文件访问、网络请求、系统调用)
- 管理员审核通过后,WASM 模块分发到实例
- 实例内 WASM Runtime 加载并执行,沙箱隔离
实例出站策略收紧(P0 修复):
P0 修复:原方案实例出站允许
0.0.0.0/0:443,恶意 Skill 可外传数据。改为白名单模式:仅允许访问已知域名。
yaml
# NetworkPolicy egress 规则(收紧后)
spec:
egress:
- to:
# 仅允许访问模型 API 域名
- ipBlock:
cidr: 100.64.0.0/16 # 模型 API 内网端点
# 仅允许访问企业 MCP Server
- ipBlock:
cidr: 10.0.100.0/24 # 企业 MCP Server 网段
# 仅允许访问基础设施
- namespaceSelector:
matchLabels:
name: qwenpaw-infra
ports:
- port: 8848 # Nacos
- port: 9000 # MinIO
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 8848
- protocol: TCP
port: 9000密钥安全原则(更新):
| 原则 | 实现 |
|---|---|
| 明文不落库 | Vault 使用 per-tenant Fernet 加密(HKDF 派生),数据库只存密文 |
| 明文不进备份 | include_secrets=false,备份不含任何密钥 |
| 明文不进日志 | secrets 卷不挂载到日志采集路径 |
| 明文不见员工 | 员工在任何界面都看不到密钥/凭证明文 |
| 每实例独立 | 每个实例的 RT 独立随机生成,绝不共用 |
| 租户隔离 | 每租户独立加密密钥(HKDF 派生),跨租户不可解密 |
| K8s Secret 加密 | Sealed Secrets 加密存储,运维人员只能看到密文 |
| Skill 沙箱隔离 | WASM 运行时执行,无法访问文件系统和网络 |
| 出站白名单 | 实例仅允许访问模型 API + 企业 MCP + 基础设施,禁止 0.0.0.0/0 |
2.5 第五层:全链路审计
职责:所有操作可追溯
审计事件格式:
json
{
"trace_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-25T10:30:00.123Z",
"event_type": "tool_call",
"source": "instance",
"user_id": "zhangsan",
"instance_id": "inst_abc123",
"session_id": "user:zhangsan:default",
"task_id": "task_abc123",
"tool_name": "file_write",
"tool_params": {"path": "/data/report.md", "size": 2048},
"result": "success",
"tokens_used": 350,
"latency_ms": 120
}三、数据安全原则
3.1 数据分类
| 分类 | 数据 | 保护级别 | 备份策略 |
|---|---|---|---|
| 机密 | RT Token、模型 API Key、MCP 凭证、IM 凭证 | Vault 加密 + K8s Secret | 不入备份 |
| 内部 | 会话记录、记忆数据、执行产物 | 实例 PVC + NetworkPolicy | 逻辑备份(加密传输) |
| 公开 | Skill 元信息、健康状态 | 无特殊保护 | 无需备份 |
3.2 数据生命周期安全
| 阶段 | 安全措施 |
|---|---|
| 创建 | RT 随机生成 → Vault 加密存储 → K8s Secret 注入 → 明文不落盘 |
| 运行 | PVC 按 user_id 隔离 → NetworkPolicy 实例间拒绝 → 审计全量留痕 |
| 暂存 | 备份不含密钥 → 加密传输到 MinIO → PVC 保留(NetworkPolicy 仍生效) |
| 恢复 | Vault 取 RT → K8s Secret 重建 → 备份恢复(不含密钥) |
| 离职 | RT 即时吊销 → 数据交接归档 → 归档到期清理 → 审计记录永久保留 |
3.3 租户隔离审计
| 审计项 | 检查方式 | 频率 |
|---|---|---|
| PVC 归属 | labels 中 owner_user_id 与实际用户一致 | 每日 |
| NetworkPolicy 生效 | 实例间网络不可达 | 每周 |
| Vault scope 隔离 | 跨租户解密尝试失败 | 每月 |
| 备份不含密钥 | 解压备份检查无敏感文件 | 每次备份 |
| 三处绑定一致 | K8s labels = Nacos metadata = registry | 每 5 分钟 |
四、安全事件响应
4.1 告警规则
| 事件 | 检测方式 | 告警级别 | 响应 |
|---|---|---|---|
| RT 暴力猜测 | 连续 5 次 401(同一来源) | 紧急 | 封禁来源 IP + 通知安全团队 |
| 密钥泄露 | 审计异常调用模式(异地/异时段) | 紧急 | 即时吊销 + 轮换 + 溯源 |
| 越权访问 | 员工角色访问管理端点 | 高 | 记录审计 + 通知管理员 |
| 异常流量 | 单实例 QPS 突增 10 倍 | 中 | 限流降级 + 人工复核 |
| 非工作时间操作 | 凌晨 0-6 点大量调用 | 中 | 告警 + 人工复核 |