Archived
170 lines
7.7 KiB
Markdown
170 lines
7.7 KiB
Markdown
# 安全模型
|
||
|
||
> **历史文档:** workload-sts 未投入生产,本安全设计已停止实施。替代方案见
|
||
> [`superseded-by-spire.md`](superseded-by-spire.md)。
|
||
|
||
| 项目 | 内容 |
|
||
| --- | --- |
|
||
| 状态 | Approved |
|
||
| 最后更新 | 2026-09-10 |
|
||
|
||
## 保护目标
|
||
|
||
- 只有通过可信 verifier 验证并匹配 trust binding 的 workload 才能取得 JWT。
|
||
- 签发的 JWT 只能用于明确的 audience,且权限不能超过 SA binding 的 issuance rule。
|
||
- 客户端不能控制输出 subject、任意 claims、签名算法或最大 TTL。
|
||
- subject token、输出 bearer token 和签名私钥不得进入数据库、日志、metric 或 trace。
|
||
- 一个 issuer、principal 或策略配置错误不能静默扩大为所有 audience 的访问权。
|
||
- OpenBao 与 Kubernetes 保留各自的最终授权权威。
|
||
|
||
## 信任边界
|
||
|
||
平台管理员可以配置 verifier、trust binding、audience、token profile 和 policy,因此是
|
||
本系统的高信任主体。上游 issuer 管理员能够为其信任域签发身份;其权限必须通过
|
||
trust binding 限制,不能因 issuer 被信任就获得所有 audience。
|
||
|
||
OpenBao Transit 保管 JWT 签名私钥。workload-sts 使用自己的 Kubernetes ServiceAccount
|
||
JWT 登录 OpenBao,并取得只能请求指定 Transit key 签名和读取其公钥所需信息的短期
|
||
运行凭据。该凭据与客户端通过 JWT auth 获得的 Bao token 是不同安全域。资源服务器
|
||
通过 HTTPS 获取 metadata/JWKS 或使用固定信任配置验证 JWT。
|
||
|
||
数据库管理员可以改变签发配置,等价于 IAM 管理员;数据库备份和迁移必须按高敏感配置
|
||
处理,即使其中没有 bearer token 或私钥。
|
||
|
||
## 主要威胁
|
||
|
||
### Subject token 重放
|
||
|
||
攻击者取得尚未过期的 Kubernetes JWT 或 JWT-SVID 后可能重复交换。缓解措施包括:
|
||
|
||
- 上游 token 必须使用短 TTL;
|
||
- 严格校验其 audience 为 workload-sts;
|
||
- 输出 JWT 使用短 TTL 和独立 audience;
|
||
- 高风险 profile 可以要求在线状态验证或一次性机制;
|
||
- 日志以安全指纹关联重复请求,不保存完整 token。
|
||
|
||
是否对所有 subject token 实现全局 `jti` 单次消费仍待决定;默认不能假定标准
|
||
ServiceAccount JWT 只能交换一次。
|
||
|
||
### Audience confusion
|
||
|
||
验证必须检查上游 token 的 audience,而不只检查签名和 issuer。输出 token 的 `aud`
|
||
必须来自已登记 audience,禁止接受自由字符串后原样签入 JWT。
|
||
|
||
不同 Kubernetes cluster、OpenBao 实例和普通 API 应使用不同 audience。资源服务器必须
|
||
拒绝没有自己 audience 的 token。
|
||
|
||
### Issuer confusion
|
||
|
||
不能只按 JWT `sub` 映射 principal。trust binding 至少绑定:
|
||
|
||
```text
|
||
credential type + issuer + source subject
|
||
```
|
||
|
||
同名 ServiceAccount 在不同 cluster 中是不同来源。issuer discovery、JWKS URL 和允许的
|
||
签名算法必须由管理员配置,不能由未验证 token 中的 URL 动态决定。
|
||
|
||
### Claim injection 与权限扩大
|
||
|
||
请求中的 `scope`、`audience` 和 `client_id` 都是不可信输入。`scope` 和 `audience` 只
|
||
表示请求上限;v1 的必填 `client_id` 只用于审计且没有默认值。输出 claims 由 verifier
|
||
事实、trust binding、issuance rule 和服务端 token profile 共同生成。
|
||
|
||
CEL 只能读取经过类型化的上下文。未经验证的 Gitea repository、workflow、branch、
|
||
job ID、HTTP header 或自报 SPIFFE ID 不能用于扩大权限。
|
||
|
||
### Algorithm 与密钥混淆
|
||
|
||
- 每个 issuer 固定允许的算法集合;
|
||
- 拒绝 `none` 和未配置算法;
|
||
- 不根据 JWT header 中的任意 URL 获取密钥;
|
||
- `kid` 必须解析到对应 issuer 的已知密钥;
|
||
- 签名与验证密钥用途分离;
|
||
- 轮换时同时发布当前及仍覆盖有效 token 的旧公钥。
|
||
|
||
### SSRF 与 discovery
|
||
|
||
上游 issuer、discovery URL 和 JWKS URL 是管理员配置,不从 token exchange 请求动态
|
||
创建。HTTP client 必须设置超时、响应大小上限和可接受的 TLS 信任;默认拒绝重定向到
|
||
未批准 origin。
|
||
|
||
### 策略拒绝服务
|
||
|
||
CEL 环境只暴露固定变量和函数,限制表达式大小、编译时间与运行成本。策略在发布前编译
|
||
验证;无效策略不能替换最后一个有效版本。求值错误按拒绝签发处理。
|
||
|
||
## Kubernetes ServiceAccount JWT
|
||
|
||
- 只接受 projected、带过期时间和明确 audience 的 token;
|
||
- 使用 TokenReview 校验身份和 audience,并检查返回的兼容 audience;
|
||
- cluster identity 属于 verifier 配置,不能只由 token 的通用 issuer 名推导;
|
||
- TokenReview 不可用或失败时不得自动降级为离线 JWKS;
|
||
- token 中的 namespace、ServiceAccount、Pod 和 Node 信息只有经过 TokenReview 返回的
|
||
已认证 user/extra 信息确认后才能进入 policy context。
|
||
|
||
## 未来的 SPIFFE verifier
|
||
|
||
SPIFFE 不属于第一阶段。未来接入时必须验证真正的 SVID 和 trust bundle,不能仅把内部
|
||
principal 写成 `spiffe://` URI:
|
||
|
||
- JWT-SVID 必须校验 trust bundle、audience 和时效;
|
||
- X.509-SVID 只能通过完成验证的 mTLS 连接接受;
|
||
- 不能接受普通 header 中自报的 SPIFFE ID;
|
||
- SPIFFE ID 到内部 principal 的 trust binding 必须显式配置。
|
||
|
||
## Token 签发
|
||
|
||
- 输出 JWT 必须包含 `iss`、`sub`、`aud`、`iat`、`nbf`、`exp` 和唯一 `jti`;
|
||
- 默认不签发 refresh token;
|
||
- token profile 由匹配身份的 trust binding/issuance rule 唯一确定,并定义 TTL 和允许
|
||
claims;
|
||
- 客户端可以请求更少 scope,不能请求 TTL 或扩大 scope;
|
||
- JWT 中不放置 secret、上游原始 token 或无界个人信息;
|
||
- 面向 OpenBao 的 JWT 只是登录 assertion,不是 Bao client token。
|
||
|
||
## 日志与审计
|
||
|
||
允许记录:
|
||
|
||
- request ID;
|
||
- 上游 issuer ID 和 credential type;
|
||
- 规范化 principal;
|
||
- 目标 audience、批准后的 scope/profile;
|
||
- 自报 `client_id_claimed`(每个 v1 请求必有,明确标记为未经认证);
|
||
- policy/version;
|
||
- 结果、错误类别、输出 JWT `jti` 和时间。
|
||
|
||
禁止记录:
|
||
|
||
- Authorization header;
|
||
- subject token 或输出 JWT;
|
||
- mTLS 私钥或完整证书链;
|
||
- OpenBao token;
|
||
- OAuth client secret;
|
||
- token endpoint 请求/响应体原文。
|
||
|
||
OpenTelemetry HTTP instrumentation 不得配置 request/response body 或 Authorization、
|
||
Cookie 等凭据 header 捕获。span name 和 `http.route` 只能使用固定路由模板;原始 URL
|
||
query、`client_id_claimed`、principal、subject、scope 和 `jti` 不得成为 HTTP metric
|
||
label。业务 scope/profile 指标只能使用管理员配置的受控枚举。
|
||
|
||
## 发布前安全验收
|
||
|
||
- 错误 issuer、签名、audience、算法、过期时间和未生效时间均被拒绝。
|
||
- 同名但来自另一 Kubernetes cluster 的 ServiceAccount 不能匹配 trust binding。
|
||
- 客户端不能通过请求 audience、scope、TTL 或 claims 扩大授权,且不能请求 token
|
||
profile。
|
||
- 缺少或格式错误的 `client_id` 被拒绝;改变合法 `client_id` 不改变 principal、授权、
|
||
profile 或输出 claims。
|
||
- `client_id` 必须匹配 `^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$`,在进入审计前完成校验,
|
||
防止控制字符、日志注入和无界字段大小。
|
||
- 一个面向 OpenBao 的 JWT 不能用于 Kubernetes 或其他 API。
|
||
- 删除或禁用 trust binding 后不再签发新 token。
|
||
- 删除或禁用 trust binding 不会立即撤销已经签发的自包含 JWT;最大 TTL 是该变更的
|
||
最长生效延迟。
|
||
- CEL 编译或运行失败时拒绝签发,且不破坏最后一个有效策略版本。
|
||
- 日志、错误、metrics、trace、数据库和测试 artifact 不含 canary token。
|
||
- JWKS 轮换期间,轮换前已签发且未过期的 token 仍可验证。
|
||
- OpenBao、数据库或签名后端不可用时失败关闭,不签发降级 token。
|