This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
workload-sts/docs/specification.md
T

507 lines
21 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.
# Workload STS 系统规格说明书
| 项目 | 内容 |
| --- | --- |
| 状态 | Approved |
| 版本 | v0.1 |
| 最后更新 | 2026-09-10 |
本文定义 workload-sts 的第一阶段外部合同、范围、安全边界和验收标准。内部包结构和
具体 OAuth2/JWT/HTTP 库不属于外部兼容性合同。
## 1. 背景
homelab 中的 Kubernetes workload、CI worker、长期主机以及未来的外部 attestation
系统使用不同的初始身份证明。OpenBao 能验证其中一部分身份并签发自己的 token,但
不应承担所有跨环境主体映射和 OAuth2 federation 语义。
workload-sts 在上游身份与下游资源服务器之间提供薄的 token exchange 层:验证来源、
规范化主体、执行签发策略并生成短期 JWT。OpenBao 继续作为机密、PKI、动态凭据和自身
token 生命周期的权威系统。
## 2. 目标
1. 提供符合标准 OAuth 2.0 语义的 Authorization Server metadata 与 token endpoint。
2. 实现 RFC 8693 Token Exchange 的最小可互操作子集。
3. 第一阶段支持 Kubernetes ServiceAccount JWT,并为其他 credential issuer 保留
verifier 扩展边界。
4. 将上游身份映射为稳定、不可由客户端选择的 workload principal。
5. 只为显式获准的 audience 和 scope 签发 JWT,由服务端选择唯一 token profile。
6. 使 OpenBao、Kubernetes 和普通 Resource Server 能用原生 JWT 能力完成后续认证。
7. 保持 HTTP 服务无本地状态,并记录不含 token 内容的结构化审计事件。
## 3. 非目标
1. 不实现用户密码、MFA、Authorization Code flow 或用户 consent。
2. 不成为 OpenBao API proxy,不读取或返回 secret。
3. 不签发或管理 OpenBao client token、lease、renewal 和 revocation。
4. 不定义 OpenBao policy、Kubernetes RBAC 或应用业务权限。
5. 不管理 workload、runner、Pod、VM 或 microVM 生命周期。
6. 不实现 PVE/microVM vsock metadata 和 attestation。
7. 不根据未经验证的 Gitea job 环境变量建立身份。
8. 不实现自定义策略解析器。
9. 第一阶段不支持任意第三方动态 client/issuer 注册。
10. 第一阶段不直接返回 OpenBao token、SSH certificate、数据库密码或云凭据;增加非
JWT credential materializer 必须进入新的规格版本和安全评审。
## 4. 参与者
| 参与者 | 职责 |
| --- | --- |
| Subject | 持有上游 credential 并请求 token exchange 的 workload |
| Credential issuer | 签发或证明 subject credential,并由对应 verifier 验证 |
| OAuth client | 调用 token endpoint 的软件;v1 的必填 `client_id` 是自报审计标签,不参与认证 |
| workload-sts | 验证、映射、授权并签发 JWT |
| Policy administrator | 配置 verifier、binding、audience、issuance rule 和 profile |
| Signing backend | 保管私钥并执行 JWT 签名;生产环境使用 OpenBao Transit |
| Resource Server | 验证输出 JWT 并执行本地授权,例如 OpenBao 或 Kubernetes |
## 5. 稳定标识
### 5.1 OAuth issuer
规范 issuer 为:
```text
https://identity.ad.ddupan.top
```
所有 metadata、JWKS 和 JWT `iss` 必须使用完全一致的 issuer。服务部署地址、Pod IP、
内部 Service DNS 和数据库位置不能进入 `iss`。
### 5.2 Principal
每个 principal 具有不可变 ID 和可读名称:
```text
id: <immutable identifier>
name: ci/homelab-infra-plan
```
输出 JWT 使用不可变 ID 作为 `sub`,可选的受控 claim 表示可读名称:
```json
{
"sub": "<immutable principal ID>",
"principal_name": "ci/homelab-infra-plan"
}
```
SPIFFE ID 只有在未来通过真正的 SVID verifier 验证后才作为 source subject 使用。内部
principal 不使用 `spiffe://` URI 模拟 SPIFFE 身份。
### 5.3 Audience
audience 是已登记资源服务器的稳定标识,不是任意用户输入。初始 OpenBao audience 为:
```text
https://bao.ad.ddupan.top:8200
```
每个 Kubernetes cluster 必须使用独立 audience。
## 6. OAuth 2.0 接口
### 6.1 Metadata
服务必须发布 RFC 8414 Authorization Server Metadata,至少公布:
- `issuer`;
- `token_endpoint`;
- `jwks_uri`;
- `grant_types_supported`;
- `token_endpoint_auth_methods_supported`;
- `scopes_supported`;
第一阶段的 `token_endpoint_auth_methods_supported` 只包含 `none`。
第一阶段不是 OIDC Provider,只发布:
```text
/.well-known/oauth-authorization-server
/oauth2/token
/oauth2/jwks
/healthz
/readyz
```
不发布 authorize、OIDC discovery、introspection 或 revocation endpoint。
### 6.2 Token Exchange 请求
服务必须接受表单编码的 RFC 8693 请求:
```http
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=...
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://bao.ad.ddupan.top:8200
&scope=bao.login
&client_id=homelab-infra-ci
```
第一阶段:
- `grant_type` 只接受 token exchange;
- `subject_token` 在第一阶段必填;
- `subject_token_type` 必须来自已支持集合;
- `requested_token_type` 只支持 access token;
- `audience` 必须且只能解析为一个已登记 audience;
- `resource`、`actor_token`、delegation 和 impersonation 暂不支持;
- `scope` 只能请求 grant 已允许集合的子集;
- `scope` 是保留的标准扩展点,第一阶段必须显式提交;
- `client_id` 必填且没有默认值,是调用方自报的有界审计标签,不注册、不认证且不参与
授权;
- 客户端不能提交 token profile、TTL、输出 claims、签名算法或 signing key;
- 不返回 refresh token。
第一阶段 token endpoint 不要求额外的 confidential client credential;调用者身份来自
经过验证的 `subject_token`。必填 `client_id` 只能以 `client_id_claimed` 语义进入审计,不能
作为已验证身份、授权输入或低基数 metric label。允许调用方自选 `client_id` 是本项目
刻意采用的审计语义,不代表完整 OAuth client registration;依赖已注册 client identity
的通用 OAuth 行为不在 v1 合同内。以后增加 mTLS 或 `private_key_jwt` client
authentication 必须保持 client identity 与 subject identity 分离。
缺少、为空或格式不合法的 `client_id` 返回 `invalid_request`,不推导匿名或默认 client。
未来启用 client registration/authentication 时继续使用同一必填字段,并将审计语义从
`client_id_claimed` 提升为经过验证的 client identity。
`client_id` 长度必须为 3–63,只允许 ASCII 小写字母、数字和中横线,且首尾必须是字母
或数字。规范正则表达式为:
```regex
^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$
```
### 6.3 成功响应
成功响应使用标准 OAuth2 token response:
```json
{
"access_token": "<signed JWT>",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "bao.login"
}
```
响应必须带 `Cache-Control: no-store` 和 `Pragma: no-cache`。
### 6.4 错误响应
错误必须使用 OAuth2/RFC 8693 标准错误,例如:
- `invalid_request`;
- `invalid_grant`;
- `invalid_scope`;
- `unsupported_grant_type`;
- `invalid_target`。
响应不得暴露 token 内容、签名验证细节、内部 policy 表达式或数据库信息。认证失败与
授权失败的外部响应应避免成为主体和 grant 枚举接口。
## 7. 上游凭据验证
### 7.1 通用要求
所有 JWT verifier 必须校验:
- 配置的 issuer;
- 允许的签名算法;
- signature;
- `exp`、`nbf` 和允许的时钟偏差;
- 面向 workload-sts 的 audience;
- subject 存在且满足该 issuer 的格式合同。
discovery 与 JWKS 地址来自管理员配置,不能由请求动态提供。未知 issuer、`kid` 或算法
必须拒绝,且不能回退到不校验签名的模式。
### 7.2 Kubernetes ServiceAccount JWT
第一阶段必须支持 projected ServiceAccount JWT。每个 Kubernetes cluster 作为独立
verifier 配置,至少包含:
- cluster ID;
- issuer URL;
- workload-sts audience;
- TokenReview API 配置;
- 允许映射的 namespace 与 ServiceAccount。
第一阶段使用 TokenReview,并在 `spec.audiences` 显式提交 workload-sts audience;只有
`status.authenticated=true` 且 `status.audiences` 包含兼容 audience 时才验证成功。
TokenReview 失败或不可用时失败关闭,不自动降级为离线 JWKS。
### 7.3 未来 verifier
不同 credential issuer 使用不同 verifier 逻辑,并统一输出类型化的已验证身份。第一阶段
不实现 SPIFFE、通用 OIDC、云身份或 metadata issuer;增加它们不应改变 token endpoint
合同。
未来接入 SPIFFE 时必须使用真正的 SVID 语义:
- 验证 JWT-SVID trust bundle、audience 和时效;
- 通过已验证 mTLS 连接接受 X.509-SVID;
- 将 SPIFFE ID 作为 source subject,再显式映射到内部 principal;
- 不接受 HTTP header 或请求字段自报的 SPIFFE ID。
X.509-SVID 若作为 mTLS client authentication,预计使用 `client_credentials` 而不是
缺少 `subject_token` 的非标准 token exchange;该流程需单独规格批准。
## 8. Trust binding 与 principal
trust binding 使用以下输入匹配上游身份:
```text
credential type
upstream issuer/verifier ID
source subject
verified attributes
```
每个 binding 输出唯一规范化 principal,并携带该身份允许的 issuance rules。客户端不能
请求 principal,也不能让同一个未约束通配规则跨 issuer 匹配。credential
issuer/verifier 不是 OAuth client;自报 `client_id` 不参与 binding。
初始 Kubernetes 映射示例:
```yaml
verifier: k8s-homelab
sourceSubject: system:serviceaccount:ci:homelab-infra-plan
principalID: 01993f4d-5e1a-7000-8000-000000000001
principalName: ci/homelab-infra-plan
issuanceRules:
- audience: https://bao.ad.ddupan.top:8200
allowedScopes: [bao.login]
tokenProfile: openbao-login-jwt
```
配置模型最终采用文件、数据库 API 或管理 API 尚待决定。
## 9. 签发授权
### 9.1 结构化 issuance rule
binding 中的 issuance rule 至少关联:
```text
audience
allowed scopes
token profile
optional CEL condition
```
有效授权为已匹配 binding、issuance rule 和客户端请求的交集。客户端可以请求更少,
不能请求更多。服务端直接使用唯一匹配 rule 绑定的 token profile;零个匹配表示不允许
签发,多个匹配表示配置冲突并失败关闭。
### 9.2 CEL
项目使用现成 CEL 实现,不自行编写表达式解析器。CEL 只处理基于已验证事实的条件,
不能替代 principal、audience 和 scope 的结构化模型。
初始 CEL 环境计划提供:
```text
credential.type
credential.issuer
credential.subject
credential.attributes
principal.id
request.audience
request.scopes
```
token profile 是 binding/issuance rule 的配置,不作为 CEL 输入。具体类型、允许函数、
cost limit 和错误语义必须在实现前形成 API reference。CEL 编译或求值失败一律拒绝签发。
## 10. Token profile 与 JWT claims
token profile 由管理员定义,并由匹配当前 Kubernetes 身份的 binding/issuance rule 在
服务端确定。它至少限制:
- audience;
- 最大 TTL;
- 签名 key/algorithm;
- 允许 scopes;
- 固定 claims;
- 从 verified identity 派生的 claims 模板。
所有输出 JWT 必须包含:
```json
{
"iss": "https://identity.ad.ddupan.top",
"sub": "01993f4d-5e1a-7000-8000-000000000001",
"principal_name": "ci/homelab-infra-plan",
"aud": ["https://bao.ad.ddupan.top:8200"],
"iat": 1789062000,
"nbf": 1789062000,
"exp": 1789062300,
"jti": "<unique identifier>",
"scope": "bao.login"
}
```
客户端不能直接提供输出 claim map。面向 OpenBao 的 token 默认不携带 Bao policy 名;
OpenBao 应根据 `iss`、`sub`、`aud` 和必要的受控 profile claim 映射本地 JWT role。
第一阶段 JWT TTL 固定为五分钟,允许 30 秒时钟偏差。输出 token 不可续期且不附带
refresh token;客户端不能请求 TTL。这不约束资源服务器随后签发的本地 token。
## 11. 签名与 JWKS
生产环境通过 OpenBao Transit 完成签名,使 workload-sts 实例和数据库都不保存私钥。
workload-sts 使用自己的 Kubernetes ServiceAccount JWT 登录 OpenBao,取得只能访问指定
Transit signing key 和所需公钥信息的短期运行凭据。该凭据不是客户端通过 STS JWT
登录 OpenBao 后取得的 token。实现必须:
- 为 JWT 设置稳定 `kid`;
- 发布当前公钥及仍覆盖有效 token 的旧公钥;
- 只允许 token profile 选择的算法;
- 在签名后端不可用时失败关闭;
- 使轮换周期和旧公钥保留时间覆盖最大 JWT TTL 与允许时钟偏差。
第一阶段使用 RS256,Transit 使用 RSA key、SHA-256 和 PKCS#1 v1.5 签名。实现前的互操作
PoC 必须证明 Transit 生成的 JWS 能由项目 JWKS、OpenBao JWT auth 和 Kubernetes 外部
JWT authenticator 验证;PoC 失败则返回规格阶段重新选择算法。
## 12. 状态与数据库
实现使用 Go 1.26、PostgreSQL、`pgx/v5` 和版本化 migration。HTTP 实例必须无本地持久
状态,可在共享数据库之上水平扩展。数据库可以保存配置、策略版本和审计索引,但禁止
保存:
- subject token;
- 输出 JWT;
- Authorization header;
- OpenBao token;
- OAuth client secret 明文;
- JWT signing private key。
第一阶段不提供管理 API。声明式配置先离线校验,再通过独立 publish 命令在一个事务中
写入新的 policy version;HTTP 实例只读取已激活版本。配置写入必须具有版本和审计信息。
## 13. 审计与可观测性
每次 token exchange 至少产生:
```text
timestamp
request ID
credential verifier ID/type
upstream issuer
client_id_claimed(v1 必有,且明确标记为未经认证)
principal
audience
approved scopes/profile
policy version
allow/deny/error result
output jti(仅成功时)
```
审计不得记录任何完整 bearer token,也不得把 `client_id_claimed` 表述为已认证 client。
metrics 只能使用 verifier、audience、profile 和结果等受控低基数字段;不能把自报
`client_id`、subject、`jti` 或请求字符串作为 metric label。
## 14. 失败语义
- verifier、数据库、策略或签名后端不可用时失败关闭;
- 未知 issuer、principal、audience、scope 或 profile 均拒绝签发;
- CEL 编译/求值失败拒绝签发;
- 客户端断开和服务超时必须取消下游 discovery、JWKS、数据库及签名请求;
- 重试可能产生多个仍然有效的输出 JWT,因此资源服务器不能假定一次请求只有一个
`jti`;
- 内部错误对客户端返回稳定 OAuth2 错误,对日志记录无 token 的诊断原因。
## 15. 第一阶段验收标准
1. metadata 正确公布 issuer、token endpoint、JWKS 和支持的 grant/type。
2. 合法 Kubernetes projected SA JWT 可映射到唯一 workload principal。
3. 错误 issuer、signature、audience、算法、`exp` 或 `nbf` 的 JWT 被拒绝。
4. 未配置 trust binding、audience、scope 或 profile 的请求被拒绝。
5. 客户端请求超过 issuance rule 的 scope 时不能扩大权限,也不能提交 TTL。
6. CEL 条件只读取类型化的已验证事实;编译或求值错误拒绝签发。
7. 成功响应符合 RFC 8693,并带 `no-store`/`no-cache`。
8. 输出 JWT 具有正确 `iss`、`sub`、`aud`、时效、`jti` 和批准后的 scope。
9. OpenBao JWT auth 可以验证输出 JWT,并按自己的 role/policy 返回受限 client token。
10. workload-sts 不接收、记录、续期或撤销该 Bao token。
11. 面向 OpenBao 的 JWT 被配置为 Kubernetes audience 的验证器拒绝,反向亦然。
12. workload-sts 使用自己的 Kubernetes identity 取得最小权限 Bao token,并且只能调用
预期 Transit key;客户端的 Bao token 不经过 workload-sts。
13. 签名密钥轮换期间,旧的未过期 JWT 仍可通过 JWKS 验证。
14. 日志、错误、metrics、trace、数据库和测试 artifact 不含 canary token。
15. 两个服务实例共享数据库时,对相同配置产生一致授权结果和有效 JWT。
16. 客户端不能选择 token profile;同一身份、audience 和 scope 匹配多个 issuance rule
时拒绝签发。
17. 删除或禁用 trust binding 后不再签发新 token,已有 JWT 最迟在最大 TTL 后失效。
18. 每个请求必须显式提交合法 `client_id`,且没有默认值;v1 只将其作为
`client_id_claimed` 进入审计,改变它不改变
principal、issuance rule、profile、scope 或输出 claims。
## 16. 已确认决策
- 项目名称为 `workload-sts`。
- 使用标准 OAuth2 Token Exchange 语义,不设计私有 exchange 协议。
- 项目职责截止于验证身份、授权签发并生成 JWT assertion。
- OpenBao token 及其 TTL、renewal、revocation、lease 和应用清理由下游负责。
- OpenBao 保持机密、PKI、动态凭据和最终 policy 的权威后端。
- Kubernetes 可以作为上游 issuer,也可以原生验证 workload-sts 的外部 JWT;两个方向
使用不同 issuer、audience 和用途。
- `scope` 作为标准扩展点保留并由客户端显式提交;客户端不能选择 token profile。
- `client_id` 必填且没有默认值;v1 是调用方自报的审计标签,不注册、不认证且不参与
任何安全决策,未来 client authentication 继续复用同一字段。
- `client_id` 使用 3–63 字符的 DNS-label-like 小写字母、数字和中横线语法,中横线不能
位于首尾。
- 权限条件使用现成策略实现;第一阶段选择 CEL,不自行实现表达式解析器。
- 内部 principal 使用不可变 ID;SPIFFE 只有在验证真正 SVID 时才作为上游身份,不使用
`spiffe://` URI 模拟 SPIFFE 语义。
- PVE/microVM vsock metadata 与 attestation 由其他项目负责。
- Gitea Actions 运行于 Kubernetes 时使用 Kubernetes workload identity,运行于 VM 时
使用对应平台提供的标准 workload identity。
- 服务是无状态 HTTP 加数据库,部署位置不是协议或信任边界。
- OAuth issuer 使用 `https://identity.ad.ddupan.top`。
- 第一阶段只实现 Kubernetes projected ServiceAccount JWT verifier;其他 issuer 使用独立
verifier 扩展。
- workload-sts 使用自己的 Kubernetes ServiceAccount identity 获取最小权限 Bao 运行
token,以调用 OpenBao Transit 签名。
- 第一阶段只输出 JWT;直接返回其他凭据属于未来 credential materializer 的新规格。
- 第一阶段使用 Go 1.26、Kubernetes TokenReview、PostgreSQL、`pgx/v5`、UUIDv7
principal、五分钟 JWT TTL 和 30 秒时钟偏差。
- token profile 直接由匹配 Kubernetes 身份的 trust binding/issuance rule 确定。
- 第一阶段使用 OpenBao Transit RS256,并在实现前完成 JWKS、OpenBao 和 Kubernetes
外部 JWT authenticator 的互操作 PoC。
- 第一阶段只发布 RFC 8414 metadata、token、JWKS、healthz 和 readyz,不发布 OIDC、
authorize、introspection 或 revocation endpoint。
- 第一阶段不提供管理 API,声明式配置经独立命令校验后原子发布为数据库 policy version。
- 首轮 PoC 决定不采用 Fosite:v0.1 使用 Go 标准 HTTP/form 处理窄 RFC 8693 endpoint,
使用 `go-jose/v4` 做 JOSE/JWK 互操作,并通过官方 `openbao/api/v2` 调用 Transit。
- HTTP 路由层使用 `chi/v5`,保持 handler 和 middleware 与标准 `net/http` 兼容;OAuth
表单字段仍由协议层显式解析,不使用自动 request binding。
- 可观测性使用 OpenTelemetry Go trace 与 metric SDK,HTTP server 使用 `otelhttp`;SDK
exporter/reader 由进程启动配置注入,不在协议层固定 OTLP gRPC、OTLP HTTP 或具体后端。
第一阶段日志使用结构化日志并关联 trace/span ID,不要求启用 OpenTelemetry Logs SDK。
## 17. 待批准决策
- 基于已验证 principal 的限流策略;
- CEL 类型系统、cost limit 和策略发布流程;
- SPIFFE、通用 OIDC、云 workload identity 和外部 metadata issuer 的后续 verifier 合同;
- 非 JWT `requested_token_type` 与 credential materializer 的安全和响应合同。
## 18. 规范性参考
- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749)
- [RFC 7519 — JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519)
- [RFC 8414 — OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414)
- [RFC 8693 — OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693)
- [RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens](https://www.rfc-editor.org/rfc/rfc9068)
- [Kubernetes Service Accounts](https://kubernetes.io/docs/concepts/security/service-accounts/)
- [Kubernetes Authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/)
- [SPIFFE Concepts](https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/)