17 KiB
安全模块
1. 安全目标
cpa-ext 处理三类高价值资产:下游用户 Key、金额账本、上游 OAuth/Auth。它又以 native dynamic library 运行在 CPA 进程内,因此安全目标不只是“接口有密码”,而是:
- 未通过 cpa-ext 认证和金额准入的用户不能调用上游;
- 插件缺失、panic、fuse、配置失败或数据库故障时不能退化成免费放行;
- 一个下游 Credential 只能读取和消费其授权的 BillingAccount;
- 金额、价格版本和账本不能被静默覆盖或重复结算;
- downstream Key、Management Key、HMAC keyring、OAuth Token 和请求内容不泄露;
- 管理写操作可追责、可回滚且不能通过 CSRF/SSRF/路径注入扩大权限;
- native 插件故障不应无限阻塞、退出或破坏 CPA 进程。
2. 信任边界
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”不是完整安全边界。生产必须同时配置:
- 一个高熵、随机、只由运维离线保管的 CPA native sentinel key;
- 不把 sentinel 发给用户、前端、自动化调用方或日常网关;
- 所有用户只获得 cpa-ext 自管 Key;
- 外部 gateway 在 cpa-ext readiness 和负向认证探针通过前不开放用户端口;
- 插件 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/账本/价格依赖故障必须返回:
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 格式与生成
建议格式:
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 兼容一次,不能把 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 503,frontend 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,而不是只写在文档里。