Files
cpa-plugin/docs/modules/security.md
T

365 lines
17 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.
# 安全模块
## 1. 安全目标
cpa-ext 处理三类高价值资产:下游用户 Key、金额账本、上游 OAuth/Auth。它又以 native dynamic library 运行在 CPA 进程内,因此安全目标不只是“接口有密码”,而是:
1. 未通过 cpa-ext 认证和金额准入的用户不能调用上游;
2. 插件缺失、panic、fuse、配置失败或数据库故障时不能退化成免费放行;
3. 一个下游 Credential 只能读取和消费其授权的 BillingAccount
4. 金额、价格版本和账本不能被静默覆盖或重复结算;
5. downstream Key、Management Key、HMAC keyring、OAuth Token 和请求内容不泄露;
6. 管理写操作可追责、可回滚且不能通过 CSRF/SSRF/路径注入扩大权限;
7. native 插件故障不应无限阻塞、退出或破坏 CPA 进程。
## 2. 信任边界
```text
Untrusted user/client
│ downstream key + model request
Reverse proxy / readiness gate
CLIProxyAPI HTTP + auth pipeline
│ in-process C ABI / JSON RPC
cpa-ext native plugin ───── SQLite + HMAC keyring
│ host callbacks
CPA Auth manager / provider network
Administrator ── CPA Management auth ── cpa-ext Management API
Browser resource ── unauthenticated static GET / user-key-protected GET
```
信任结论:
- plugin binary 是与 CPA 同权限的受信代码,不是隔离沙箱;
- Management Key 信任的是 CPA 管理员,不自动代表具体个人;
- ResourceRoute 默认完全不可信;
- scheduler candidate 是宿主过滤后的瞬时候选,不是完整账户清单;
- provider response、models.dev、Auth 文件 display metadata 和所有 HTTP 输入都必须验证。
## 3. 必须始终成立的安全不变量
- 生产 `frontend_auth_provider_exclusive=true`
- CPA 原生 `api-keys` 中不保存任何分发给用户的 Key
- 未绑定账户/套餐/价格默认拒绝;
- 核心依赖不健康时新请求 fail closed
- 用户核心额度单位只有 Money
- 账本 append-only,余额只是可重建投影;
- secret 明文只在签发/轮换首次响应存在;
- 下游 Key 不出现在 URL、日志、事件、数据库明文字段或前端持久存储;
- 上游 Auth 原文只允许存在于最小 provider adapter 的短生命周期缓冲;
- 所有业务拒绝都走 CPA 实际会执行的终止协议;
- 权限由服务端 scope 构造,不能信任调用方传入 account/credential ID
- 插件无法独自证明的宿主状态由部署网关强制检查。
## 4. 当前 CPA 的 Critical 限制
### 4.1 Exclusive 只在插件 active 时存在
当前 CPA 在插件未加载、被 disable、register/reconfigure 失败或 fuse 后会重建 frontend providers。exclusive 可能被清除,CPA 原生认证重新生效;零 provider 还可能走 legacy 放行路径。
因此“插件声明 exclusive”不是完整安全边界。生产必须同时配置:
1. 一个高熵、随机、只由运维离线保管的 CPA native sentinel key
2. 不把 sentinel 发给用户、前端、自动化调用方或日常网关;
3. 所有用户只获得 cpa-ext 自管 Key
4. 外部 gateway 在 cpa-ext readiness 和负向认证探针通过前不开放用户端口;
5. 插件 route 消失、状态不健康或负向探针异常时立即撤流量。
sentinel 的作用是防止“零 native provider”变成隐式放行,并确保插件失效时普通用户 Key 仍不被 CPA 原生认证接受。它不是备用共享 Key。
### 4.2 Reconfigure 失败会撤掉 active record
当前 `Host.ApplyConfig` 中,已经加载的插件如果 `plugin.reconfigure` 返回错误,会被排除在新 active records 外,随后可能清除 exclusive。
所以已注册后的无效热配置必须:
- 插件内部保留 last-known-good Runtime
- 记录 `reconfigure_rejected`
- 仍返回 success registration 和上一次相同 capability shape
- 由 API/诊断提示配置未生效。
只有 CPA 宿主先实现“失败保留旧 active record”后,插件才可以安全返回 reconfigure error。
### 4.3 Interceptor error/panic 是 fail-open
CPA 对 request interceptor 的 Go error 或越过边界的 panic 会记录并继续原请求。所有预期拒绝、DB/账本/价格依赖故障必须返回:
```text
RPC envelope: ok=true
RequestInterceptResponse:
Terminate=true
StatusCode=合适的 4xx/503
ResponseBody=脱敏稳定错误
```
dispatcher 在插件内部 recover,并把 panic 转为上述 503 termination。若 panic 已越过 native 边界导致宿主 fuse,纯插件无法保证当前请求,必须由 gateway/sentinel 防线兜底。
### 4.4 Scheduler error/panic/无效响应会 fallback
strict binding 不能只依赖 `scheduler.pick`。每个 Execution 的 after-auth interceptor 必须复核实际 `selected_auth_id`,不属于允许集合就正常返回 `Terminate=true`。宿主 fuse 场景仍依赖外部门禁。
### 4.5 Handler 前宿主会 ReadAll
frontend auth 和 Management handler 在插件校验前已经收到完整 Body copy。插件内部 413 不能保护 CPA 内存,反向代理/CPA HTTP 层必须限制 body、header、连接数和读取时间。
### 4.6 Resource routes 未鉴权
ResourceRoute 只能放静态资源、最小 readiness 和自行校验 downstream Key 的用户只读 JSON。绝不允许 `host.auth.get/save`、管理员数据、备份、价格发布或任意写操作。CPA 官方 auth-files callback example 是能力演示,不是生产安全模板。
## 5. 下游 Credential 安全
### 5.1 Key 格式与生成
建议格式:
```text
cpae_<public_id>_<random_secret>
```
- `public_id` 只用于快速定位 Credential,可公开但不可授权;
- `random_secret` 至少 256 bit CSPRNG
- 完整 Key 只在首次签发/轮换响应显示一次;
- 前缀和 preview 用于人工识别,不足以认证;
- Key 必须可按 SecretVersion 独立撤销、过期和轮换。
### 5.2 存储与校验
数据库保存:
- public ID
- `HMAC-SHA-256(keyring_secret, full_key)` digest
- `hmac_key_id`
- preview、状态、创建/过期时间。
不保存可恢复明文。HMAC secret 来自环境指向的受限 secret file/OS secret store,不能写入 CPA YAML、SQLite、日志、metadata 或备份包。比较使用 constant-time compare。
高熵随机 Key 使用 keyed digest 足够;不能改为普通无密钥 SHA-256。若未来允许用户自选短密码,必须改用专门 password KDF,不能沿用此模型。
### 5.3 Keyring 轮换
- keyring 每个 secret 有稳定 key ID
- 新 Credential 使用 active key
- 旧 digest 继续按原 key ID 校验;
- 缺失未知 key ID 时认证 fail closed 并使 readiness 失败;
- 不能启动时自动生成替代 secret;
- SQLite 与 keyring 分开加密备份、成对恢复并校验 fingerprint。
## 6. 身份认证与授权
### 6.1 模型请求
frontend auth 只完成 Credential lookup 和 principal 建立。金额、状态、模型、tier、endpoint、并发和 route policy 在 before-auth/Core 准入再次检查。
principal metadata 只携带 opaque Credential/BillingAccount IDs 和最小 scope,不携带 raw Key、digest、余额或可篡改 JSON。
### 6.2 用户查询
用户 Resource API 每次自行校验 Bearer downstream Key,并由 Credential 反查服务端 scope
- 共享 BillingAccount 可以看共享金额余额;
- 默认逐请求只看当前 Credential
- 调用方传入 ID 只能缩小,不能扩大 scope;
- unknown/revoked/expired 使用统一 401,防止枚举;
- 登录尝试按 IP + public ID preview + 全局维度限流;
- Key 不进入 URL/cookie/localStorage,请求和响应 no-store。
### 6.3 管理员
Management API 继承 CPA Management middleware
- 生产 `remote-management.allow-remote=false`,或只经受信 TLS/mTLS 管理网关暴露;
- 使用随机高熵 Management Key,不复用 sentinel/downstream/HMAC secret
- 浏览器只在当前页面内存保存 Key,刷新即失效;
- cpa-ext 不把入站 Authorization 传给 Core、日志、host HTTP 或 provider
- 每个写操作需要 reason、revision 和审计;
- CPA 共享 Key 不能提供真实个人 RBAC。需要多管理员角色时增加可信 identity gateway/sidecar。
测试环境可以按用户约定使用 `000` 便于联调,但仅限 loopback、无真实 OAuth/生产数据、端口不对外开放的临时环境。任何可被其他机器访问或接入真实账户的部署都禁止使用 `000`
## 7. 租户与数据隔离
- Repository 查询必须接收已验证 `ViewerScope`,不接受裸 account ID 作为授权;
- 管理 DTO 与用户 DTO 分开定义,不能返回管理员对象后由前端隐藏;
- 缓存键包含 viewer scope,不能跨 Credential 复用敏感响应;
- cursor 绑定 scope 和过滤条件,不能拿另一账户 cursor 继续翻页;
- 导出任务固定 scope、时间范围和创建者;
- 账本 adjustment、route binding、价格发布均检查对象 revision;
- 统计聚合中低基数结果也不能绕过用户 scope。
## 8. 上游 Auth 与网络安全
### 8.1 最小读取
账户目录只调用 `host.auth.list/get_runtime``host.auth.get` 返回原始 Auth JSON,只能由 provider-specific quota adapter 在管理员触发的有界命令中读取。
raw Auth 数据:
- 不进入 domain DTO、generic map、error、trace 或数据库;
- 不跨 goroutine/channel
- 从最小缓冲提取必要字段后尽快覆盖并释放;
- 任何失败只输出分类,不包含 body/header/token。
Go/OS 无法保证内存立即物理清零,因此更安全的长期方案是 CPA 提供 provider-scoped quota callback,不把 secret 交给插件。
### 8.2 SSRF 防护
Quota/provider adapter 的 URL、method、headers 和 body 均由代码定义:
- 只允许 HTTPS
- 精确 allowlist host + path,不允许重定向到新 host;
- 禁止 localhost、私网、link-local、file/unix scheme
- 调用方只能传内部 UpstreamAccountID
- 限制响应 body 和 header 大小;
- 独立超时、并发和速率限制;
- 不把 CPA Management Authorization 转发给 `/api-call` 或第三方;
- 代理配置由宿主/运维控制,不允许用户覆盖。
## 9. 金额和数据完整性
- 金额/费率使用整数定点和有理数倍率;
- PriceVersion 发布后不可变;
- canonical Usage revision 使用 CAS/idempotency
- 账本 append-only,删除 API 不存在;
- adjustment/refund 是新账本项,必须含 reason/actor
- DB 事务同时写事实、账本、projection event/outbox
- checkpoint 可从事实重建并做总额 reconciliation
- 时间来自服务端 UTC,调用方时间只作为未经信任 metadata;
- 重复、乱序、迟到 callback 不能重复扣费;
- SQLite 失败后不得改用内存余额继续免费运行。
数据库文件、WAL、备份目录只允许 CPA 运行用户访问。备份使用 SQLite online backup API;恢复前校验 schema、integrity、账本和 keyring fingerprint。
## 10. 管理 API 与浏览器安全
- exact route + method 二次校验;
- strict JSON、字段/集合/响应大小限制;
- 所有动态 HTML/文本安全转义;
- React 只用 text node,不使用 `dangerouslySetInnerHTML`
- CSP、nosniff、no-referrer、no-store
- 默认无 CORS
- 不注册 service worker
- CSV 防公式注入;
- 用户和管理员 secret 不进入 query/fragment
- Management JSON 的宿主 entity 转义按 [api.md](api.md) 兼容一次,不能把 entity decode 后内容当 HTML。
如果未来使用 cookie session:必须在独立 authenticated route/sidecar 设计 HttpOnly、Secure、SameSite、CSRF token、Origin 校验、登录限流和 session revoke。当前插件 resource GET 不具备这些条件。
## 11. Native 插件与供应链
### 11.1 运行时
- ABI 入口只做 byte copy、JSON envelope、C buffer ownership
- C 返回内存由匹配的 plugin free 释放,不返回 Go heap pointer
- 不使用 `os.Exit``log.Fatal` 或故意 panic
- dispatcher/Core 边界 recover,错误脱敏;
- shutdown 幂等且有界;
- 不持锁执行 callback、网络或慢 SQL
- 双 Go runtime/SQLite/worker 必须通过 24h soak
- gate 未通过前动态库零长期 worker,复杂任务移到 sidecar/运维命令;
- 二进制升级排空并重启 CPA,不依赖 hot reload 回收旧 runtime。
### 11.2 发布物
- 每 OS/arch 独立构建;
- 记录 Go/C toolchain、CPA commit、ABI/schema 和源码 commit
- 归档只含动态库、LICENSE/NOTICE、版本元数据;
- 发布 SHA-256 checksums,最好增加签名/透明 provenance
- 安装前验证 checksum、文件名和目标目录;
- 不从未知 plugin store/source 安装;
- 发布包不含 config、SQLite、keyring、日志、Auth 或测试 secret
- 复制两个 MIT 项目代码时维护 THIRD_PARTY/NOTICE 和来源 revision。
## 12. 日志、诊断与审计
禁止记录:
- Authorization、X-Management-Key、Cookie
- downstream full key/HMAC digest
- OAuth/API token、Auth JSON、代理凭证;
- Prompt、Response、原始失败 body
- database absolute path(普通用户可见日志);
- raw config YAML。
允许记录:
- stable opaque IDs
- Key preview
- provider/model 分类;
- Money、状态、事件 ID
- 脱敏 error code
- callback/RPC 大小和耗时;
- readiness component 状态。
日志参数采用结构化白名单 DTO,不接受任意 wire object。安全审计和调试日志分开保留,审计不可由普通“清空日志”操作删除。
## 13. 可用性与资源滥用
- downstream/API Key 级并发和速率限制;
- BillingAccount 默认并发 1
- Management 写操作串行化到明确对象/revision,不用全局长锁;
- query 时间范围、分页、维度和导出上限;
- SQLite busy timeout 有界,锁等待产生 metric
- quota refresh 去重、批量/并发/冷却;
- stream callback 性能门禁,正常 chunk 成本不能随 prompt/history 线性放大;
- 请求体上限在反向代理先执行;
- readiness unhealthy 时撤流量,不能让重试风暴打满 DB/provider。
## 14. 威胁与控制矩阵
| 威胁 | 主要控制 |
| --- | --- |
| 插件缺失/fuse 后免费调用 | exclusive + sentinel + required readiness gateway + 负向探针 |
| DB 故障被 interceptor error 忽略 | success envelope + Terminate 503frontend auth 第二道 fail closed |
| strict binding 回退其他账户 | scheduler 决策 + after-auth 实际 Auth 验证 |
| 用户 Key 枚举/泄露 | 高熵 Key、HMAC、统一 401、限流、no-store、日志白名单 |
| 跨租户查询 | 服务端 ViewerScope、scope-bound cursor/cache、独立 DTO |
| 余额/价格篡改 | revision、不可变 PriceVersion、append-only ledger、审计 |
| quota SSRF/凭证代理 | 固定 provider adapter、HTTPS host/path allowlist、无任意 URL/header |
| Management Key 泄露 | loopback/管理网、TLS、高熵、仅内存、响应/日志删除 |
| 大 body/慢请求 DoS | 入口代理 limit/read timeout、分页/响应预算 |
| 恶意动态库/供应链 | checksums/signature/provenance、受信 registry、最小发布包 |
| SQLite/backup 被复制 | OS ACL、加密备份、keyring 分离、secret 不落库 |
| 重复/迟到 Usage 重复扣费 | event/idempotency key、canonical revision、CAS、差额账本 |
## 15. 事件响应
### 15.1 downstream Key 泄露
撤销对应 SecretVersion → 刷新内存 snapshot → 确认新请求 401 → 签发新版本 → 审计受影响窗口 → 检查异常金额/路由。
### 15.2 Management Key 泄露
立即从 gateway 撤下管理入口 → 轮换 CPA Management Key → 审查价格、adjustment、Key、route、backup/export 审计 → 必要时恢复已验证 DB backup。
### 15.3 HMAC keyring 泄露
视为所有 downstream Key 校验材料泄露。先撤流量,再增加新 active HMAC key、轮换所有 Credential SecretVersion、保留旧 key 仅完成迁移,最后撤销旧 key。仅轮换 keyring 而不轮换用户 Key 不足以消除风险。
### 15.4 OAuth/Auth 泄露
在 CPA/provider 侧撤销 OAuth → 将 UpstreamAccount 标不可用 → 检查 quota adapter、日志、dump 和 backup → 重新登录并人工 reconciliation,不自动继承旧绑定。
### 15.5 账本/数据库疑似损坏
gateway fail closed → 保存现场副本 → integrity/reconciliation → 用匹配 keyring 的已验证备份离线恢复 → 从权威事实重建投影 → 对差异使用显式 adjustment,不手改账本行。
## 16. 验收标准
- 插件缺失、disable、register/reconfigure 失败、panic、fuse 和 Home 模式均无法让普通用户 Key 访问上游;
- interceptor/scheduler 的 error、panic、timeout 和无效响应完成故障注入;
- DB、keyring、price、ledger 不健康时新请求 fail closed
- downstream/Management/HMAC/OAuth secrets 不出现在数据库、日志、API、core dump 测试样本或发布包;
- 用户跨账户 ID/cursor/cache 尝试全部失败;
- resource route 枚举证明没有未鉴权写操作或管理员数据;
- SSRF 测试覆盖重定向、私网、DNS 变化、超大响应和恶意 URL;
- 金额/价格/账本篡改和重复事件被 revision/hash/idempotency 检测;
- 入口大小限制在 CPA ReadAll 前生效;
- 动态库检查、checksum、NOTICE 和构建 provenance 完整;
- 安全事件均有可执行 runbook 和审计证据;
- 所有门禁纳入 [test-plan.md](test-plan.md),而不是只写在文档里。