From cc30a7cb6a40db288f00476113725fe480850781 Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Fri, 11 Sep 2026 15:44:26 +0000 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=E8=AE=BE=E8=AE=A1=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 107 +++++++++ README.md | 105 +++++++++ docs/architecture.md | 183 +++++++++++++++ docs/poc.md | 114 ++++++++++ docs/security.md | 166 ++++++++++++++ docs/specification.md | 506 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1181 insertions(+) create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 docs/architecture.md create mode 100644 docs/poc.md create mode 100644 docs/security.md create mode 100644 docs/specification.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..09888c4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,107 @@ +# workload-sts - AI Agent Guide + +## Specification-Driven Development + +- 所有新功能和外部可观察行为变更都采用 specification-first:先更新 + `docs/specification.md` 或对应设计文档,再编写实现。 +- 规格必须在实现前说明范围、非目标、协议行为、验证规则、失败语义、安全边界和验收 + 标准,并达到可由人工作出待决策选择的详细程度。 +- 规格处于 `Review` 时,只能完善设计、测试计划和不冻结实现的实验;获得人工批准前, + 不得把待决策项当成已确认合同实现。 +- 从批准的验收标准派生测试,再实现使测试通过的最小纵向切片。 +- 实现过程中发现歧义或需要改变已批准行为时,先停止实现,更新规格并重新请求 review。 +- 规格描述外部合同和设计决策;除非构成兼容性合同,不要过早固定 Go package、函数名、 + SQL schema 或内部模块结构。 + +## Project Boundary + +本项目只负责: + +```text +验证上游 workload credential + -> 映射规范化 principal + -> 判断是否允许为目标 audience/scope 签发 + -> 签发短期 JWT assertion +``` + +以下内容不属于本项目: + +- OpenBao secret、policy、client token、lease、renewal 和 revocation; +- Kubernetes RBAC 和 ServiceAccount 生命周期; +- Gitea Actions job、runner 和 workflow 调度; +- PVE/microVM 生命周期、vsock metadata 和平台 attestation; +- 人工用户目录、密码、MFA 和交互式登录。 + +不要为了完成下游集成而把这些职责拉入 workload-sts。需要时只定义标准协议边界和测试 +替身。 + +## Protocol and Identity Rules + +- 优先实现并复用 OAuth 2.0、RFC 8693、JWT、SPIFFE 和 Kubernetes 的标准语义;不得在 + 有成熟标准或库可用时发明私有协议、token 格式或表达式解析器。 +- OAuth issuer 固定为 `https://identity.ad.ddupan.top`;变更 issuer 是破坏性迁移,必须 + 先形成单独规格和迁移计划。 +- SPIFFE 是未来可接入的上游身份协议;第一阶段不把内部 principal 表示成 SPIFFE ID, + 也不以 URI 命名代替真正的 SVID 验证。 +- 严格区分上游 credential audience 与输出 JWT audience。Kubernetes SA JWT 用于向 + workload-sts 证明身份;workload-sts JWT 用于访问 OpenBao、Kubernetes 或其他明确的 + Resource Server。 +- 客户端不能控制输出 `sub`、任意 claims、签名算法或最大 TTL。 +- `client_id` 必填且没有默认值。v1 中它是未经认证的调用方自报审计标签;不得用于 + principal mapping、授权、输出 claims 或 metric label。未来启用 client authentication + 时继续使用同一字段,不通过省略字段表达匿名或默认 client。 +- `client_id` 必须匹配 `^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$`,即长度 3–63,且中横线 + 不能位于首尾。不得在不同入口使用更宽松的验证规则。 +- trust binding 至少绑定 credential type、issuer/verifier 和 source subject;不能只按 + `sub` 建立跨 issuer 信任。 +- SA trust binding 中的 issuance rule 限制 principal、audience 和 scope,并直接绑定唯一 + token profile;CEL 只处理经过验证的类型化上下文条件。客户端不能请求 profile。CEL + 错误必须失败关闭。 +- 面向 OpenBao 的 JWT 只是登录 assertion。不得在 workload-sts 中调用 Bao 登录接口或 + 管理随后签发的 Bao token。 + +## Security-Critical Handling + +- subject token、输出 JWT、Authorization header、OAuth client secret、OpenBao token、 + mTLS 私钥和签名私钥不得进入日志、metric、trace、数据库、测试快照或错误消息。 +- 禁止记录 token endpoint 请求/响应体原文。审计只记录 request ID、受控 issuer/verifier + ID、principal、audience、批准后的 scope/profile、policy version、结果和输出 `jti`。 +- JWT verifier 必须校验配置的 issuer、允许算法、signature、audience、`exp` 和 `nbf`; + 禁止只 decode 不 verify。 +- discovery/JWKS URL 必须由管理员配置。不得根据未验证 token 的 header 或 claim 动态 + 获取任意 URL。 +- 未知 issuer、principal、audience、scope、profile、`kid` 或算法必须拒绝;依赖不可用 + 时失败关闭,不得签发降级 token。 +- 测试必须使用明显的假 token 和独立密钥。不得复制 homelab 的真实 ServiceAccount + token、SVID、OpenBao token 或 OAuth 凭据到仓库和测试 artifact。 +- 生产签名私钥不得存入数据库或普通配置。workload-sts 可以使用自己的 Kubernetes + identity 获取最小权限 Bao 运行凭据来调用 Transit;不得与客户端的 Bao token 混淆。 + 签名后端与轮换合同以已批准规格为准。 + +## Documentation Responsibilities + +- `README.md` 是项目入口,只描述当前能力、边界和文档导航,不替代规范。 +- `docs/specification.md` 是外部行为、验收标准和已确认决策的权威来源。 +- `docs/architecture.md` 解释组件与信任边界;与规范冲突时以规范为准。 +- `docs/security.md` 记录威胁、缓解措施和发布前安全验收。 +- 已确认决策与待批准决策必须分开。不要把提案、候选默认值或 PoC 结果写成既定事实。 +- 修改协议或 claim 合同时,同时检查 README、architecture、security 和测试是否需要同步。 + +## Human-Reviewable Changes + +- 每次改动聚焦一个行为或决策,不混入无关重构、依赖升级、格式化或清理。 +- 保持每个提交边界可独立 review,并在可行时保持构建和相关测试通过。 +- 达到一个完整提交边界时,先询问用户是否创建 commit;不要未经明确授权提交或推送。 +- 创建或提交 PR 前必须再次请求用户明确授权。提交或推送许可不等于 PR 许可。 +- commit message、PR、issue 和项目文档默认优先使用中文;代码标识符、命令、配置键、 + RFC 名称和使用英文可避免歧义的技术字段可以保留英文。 + +## Verification + +- 文档改动至少运行 `git diff --check`,并检查链接、示例与已确认决策一致。 +- 实现改动运行与该纵向切片直接相关的格式化、静态检查和测试;具体命令在工具链确定后 + 写入本文件及开发文档。 +- OAuth2/JWT/SPIFFE 互操作行为必须用真实标准实现或官方测试向量验证,不能只测试项目 + 自己的 signer 与 verifier 能互相接受。 +- 安全相关测试至少覆盖错误 issuer、signature、audience、算法、过期时间、未生效时间、 + claim 注入、权限扩大和 token 泄漏。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc65725 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# workload-sts + +面向 workload identity 的 OAuth 2.0 Security Token Service。它验证 Kubernetes +ServiceAccount JWT 等上游身份,执行签发授权,并生成短期、限定 audience 的 JWT +assertion。 + +项目只负责“验证身份后签发 assertion”。OpenBao 等下游资源服务器自行验证 JWT、 +映射本地权限并管理自己的 token、lease 和凭据生命周期。 + +项目目前处于 PoC 阶段,尚未提供可运行服务或稳定 API。RFC 8693 请求约束和 OpenBao +Transit RS256/JWKS 互操作已有可执行测试。 + +## 目标 + +- 按 OAuth 2.0 Authorization Server 和 Token Exchange 标准语义提供接口,优先复用 + 成熟协议库。 +- 第一阶段验证 Kubernetes projected ServiceAccount JWT,并为其他 credential issuer + 保留独立 verifier 扩展点。 +- 将已验证的上游身份映射为稳定的 workload principal。 +- 根据 principal、audience、scope 和请求上下文判断是否允许签发,由服务端选择唯一的 + token profile。 +- 要求每个请求显式提交 `client_id`;v1 不认证该字段,只以 `client_id_claimed` 记录, + 为以后注册 public/confidential client 保持稳定请求形状。 +- `client_id` 长度为 3–63,只允许小写字母、数字和中横线,且必须以字母或数字开头和 + 结尾。 +- 使用短期 JWT 隔离上游证明与下游访问凭据;一个 token 只面向明确的 audience。 +- 让 OpenBao 保持机密、PKI、动态凭据和自身 token 生命周期的权威后端。 +- 让 Kubernetes、OpenBao 和普通 OAuth2 Resource Server 使用各自的原生 JWT 验证 + 能力,而不要求业务系统理解上游身份来源。 +- 提供结构化且不泄露 bearer token 的交换审计。 +- 使用 OpenTelemetry trace/metric SDK 与标准 HTTP instrumentation,且不采集 token、 + 请求体或高基数身份字段。 + +## 非目标 + +- 不提供用户目录、密码登录、MFA 或交互式 OIDC 登录;人工身份由现有 IdP 提供。 +- 不读取、缓存或代理 OpenBao 中的机密。 +- 不替调用方登录 OpenBao,也不签发、续期或撤销调用方的 OpenBao client token;服务 + 自身可以使用独立的 Kubernetes identity 获取访问 Transit 所需的最小权限运行凭据。 +- 不定义或同步 OpenBao policy、Kubernetes RBAC 或下游应用权限。 +- 不管理 Gitea Actions job、runner、Kubernetes Pod、PVE VM 或 microVM 生命周期。 +- 不实现 PVE/microVM vsock metadata service 或平台 attestation。 +- 不自创策略表达式语言、workload identity 格式或私有 token exchange 协议。 +- 第一阶段不直接返回 OpenBao token、SSH certificate、数据库密码或云凭据;以后增加 + credential materializer 必须通过新的规格和安全评审。 + +## 核心流程 + +以 Kubernetes 中的 CI workload 登录 OpenBao 为例: + +```text +CI Pod + | + | projected ServiceAccount JWT + | aud = https://identity.ad.ddupan.top + v +workload-sts + | 1. 验证 Kubernetes issuer、signature、audience 和时效 + | 2. 映射 workload principal + | 3. 检查 OpenBao audience 的签发策略 + | 4. 签发短期 JWT assertion + v +OpenBao JWT auth + | 验证 iss、sub、aud 和 bound claims + | 映射本地 role/policy + v +OpenBao client token +``` + +`workload-sts` 的职责在签发 JWT assertion 后结束。Bao token 的 TTL、renewal、 +revocation、lease 以及 CI job 结束后的清理由 OpenBao 和调用方负责。 + +## 协议身份 + +当前设计使用: + +```text +OAuth issuer: https://identity.ad.ddupan.top +``` + +OAuth issuer 是稳定的网络协议身份,不与进程部署位置绑定。SPIFFE 可以作为未来的上游 +身份协议,但第一阶段不建立 SPIFFE trust domain,也不把内部 principal 伪装成 SPIFFE +ID。 + +## 文档 + +- 系统边界与数据流:[`docs/architecture.md`](docs/architecture.md) +- 规范行为与待决策项:[`docs/specification.md`](docs/specification.md) +- 威胁模型与安全要求:[`docs/security.md`](docs/security.md) +- 首轮库与签名 PoC 结论:[`docs/poc.md`](docs/poc.md) + +v0.1 规范已经批准;首轮 PoC 决定不引入 Fosite,使用 chi 路由、窄协议层、Go JOSE 和 +官方 OpenBao API。详见 PoC 文档。 + +## 计划中的最小纵向切片 + +1. 发布 Authorization Server metadata 和 JWKS。 +2. 验证一个 Kubernetes projected ServiceAccount JWT。 +3. 按 RFC 8693 处理一次 token exchange。 +4. 根据 SA binding 中的 issuance rule、请求 scope 和 CEL 条件决定是否签发。 +5. 签发 `aud` 指向 OpenBao 的短期 JWT。 +6. 由 OpenBao JWT auth 验证该 JWT 并返回受限 Bao token。 + +代码与发布物托管在 +`git.ddupan.top/panxiao81/workload-sts`。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e64492b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,183 @@ +# 系统架构 + +本文是 [`specification.md`](specification.md) 的架构视图。规范定义外部行为,本文解释 +组件边界;二者冲突时以规范为准。当前 v0.1 设计已经批准。 + +## 组件与数据流 + +```text + Kubernetes SA JWT + | + v + Credential Verifiers + | + v + Principal Normalization + | + v + Issuance Policy + CEL Conditions + | + v + OAuth 2.0 Token Endpoint + | + v + Audience-bound JWT Issuer + | + +----------------+----------------+ + | | + v v + OpenBao JWT auth Kubernetes/API service + | | + v v + OpenBao client token Native authorization +``` + +服务验证上游 credential,但不把它直接转发给下游。签发决策成功后生成新的 JWT, +其 issuer、audience、TTL 和 claims 由本服务控制。 + +下游资源服务器负责最终授权。OpenBao 根据 JWT auth role 和 policy 签发自己的 client +token;Kubernetes 根据外部 JWT authenticator 和 RBAC 授权。本服务不复制这些权限模型。 + +## 组件边界 + +### OAuth 2.0 协议层 + +协议层负责 Authorization Server metadata、token endpoint、标准 OAuth2 错误和响应。 +第一阶段只实现 RFC 8693 Token Exchange 所需子集,不实现交互式用户登录。 + +协议层只实现已批准的 RFC 8693 子集。HTTP 路由和 middleware 使用与 `net/http` 兼容的 +`chi/v5`,OAuth 表单仍由 Go 标准库显式解析,不使用框架自动 binding;JOSE 与 OpenBao +调用使用成熟库。首轮 PoC 发现现有 OAuth2 框架仍需自定义 token +exchange、client store 适配和 Transit signer,不能减少本项目的核心安全逻辑,因此 v0.1 +不引入完整 Authorization Server 框架。业务代码仍将 credential verification、授权和 +token materialization 保持为独立边界。详见 [`poc.md`](poc.md)。 + +HTTP router 外层使用 `otelhttp` 生成 server span 和标准 HTTP metrics。span 与 metric 只使用 +固定路由模板及受控低基数字段,不捕获请求/响应 body、Authorization header、 +`subject_token`、输出 token、`client_id`、principal 或 `jti`。trace 与 metric provider +通过启动依赖注入;未配置 exporter 时保持 no-op,不把 telemetry 输出到标准输出。 + +### Credential Verifier + +每种 verifier 把特定上游凭据转换为不可伪造的内部事实: + +```text +issuer +source subject +source credential type +source audiences +issued-at / expiry +verified attributes +verification method +``` + +verifier 不能直接决定目标 audience 或输出 claims。未经验证的请求字段不能进入 +`verified attributes`。 + +第一阶段支持: + +- Kubernetes projected ServiceAccount JWT。 + +OIDC、SPIFFE、云 workload identity 和外部 metadata 系统以后以 verifier 扩展,不改变 +token endpoint 合同。不同 credential issuer 可以使用不同验证逻辑,但 upstream issuer +不是 OAuth client。 + +所有 token request 都必须携带 `client_id`。v1 的 client authentication method 为 +`none`,因此该值只以 `client_id_claimed` 进入审计,不参与身份映射或授权。以后可以为 +同一字段增加 public client registration、mTLS 或 `private_key_jwt` 认证,无需改变请求 +是否包含 `client_id` 的合同。 + +`client_id` 使用 3–63 字符的 DNS-label-like 语法,只允许小写字母、数字和中横线,且 +中横线不能位于首尾。 + +### Principal Normalization + +trust binding 把上游 verifier 与 source subject 映射为稳定、不可变的内部 principal: + +```text +sub = 01993f4d-5e1a-7000-8000-000000000001 +principal_name = ci/homelab-infra-plan +``` + +可读名称允许受控改名,JWT `sub` 使用的 ID 不随名称变化。映射规则由管理员配置; +客户端不能在 token 请求中选择或覆盖 `sub`。一个上游身份可以没有任何映射,此时认证 +成功但 token exchange 被拒绝。 + +### Issuance Policy + +授权层只回答: + +> 某个已验证 principal 是否可以为指定 audience 和 scope 获取 JWT? + +trust binding 中的 issuance rule 先限制 principal、audience 和 scope,并直接绑定唯一 +token profile;CEL 只表达需要结合已验证上下文的条件。客户端不能请求 profile。CEL +不能调用网络、读取机密或修改状态。 + +授权层不决定 OpenBao secret path、Bao token TTL 或 Kubernetes RBAC。 + +### JWT Issuer + +JWT issuer 根据批准的 token profile 生成 claims 并请求签名后端签名。客户端只能请求 +允许缩小权限的参数,不能直接提交输出 `sub`、任意 claim、签名算法或 TTL。 + +生产签名密钥保存在 OpenBao Transit 中;数据库和服务实例不保存私钥。 +workload-sts 使用自己的 Kubernetes ServiceAccount JWT 登录 OpenBao,取得只允许访问 +指定 Transit key 的短期运行凭据。它不替客户端登录 OpenBao,也不接触客户端随后取得 +的 Bao token。第一阶段使用 RS256;签名后端保留接口边界,以便单元测试使用内存 +signer。密钥轮换的具体运维合同在部署设计中补充。 + +### Database + +HTTP 实例无本地持久状态。数据库保存配置及需要一致性的控制面状态,例如: + +- upstream issuer 与 verifier 配置; +- trust binding; +- principal、audience、scope 与 token profile; +- CEL policy 及版本; +- 调用方自报 `client_id` 的有界审计值; +- 审计索引和配置变更记录。 + +数据库不得保存 bearer token、原始 subject token、OpenBao client token 或签名私钥。 +自包含 JWT 的普通验证不依赖数据库在线 introspection。 + +## Kubernetes 信任方向 + +系统明确区分两条方向相反的链: + +```text +Kubernetes SA JWT -> workload-sts +``` + +projected ServiceAccount JWT 的 audience 是 workload-sts,用于证明 Pod 的上游身份。 + +```text +workload-sts JWT -> kube-apiserver +``` + +目标 audience 是特定 Kubernetes cluster。kube-apiserver 通过外部 JWT authenticator +验证 issuer 和 claims,再交由 RBAC 授权。workload-sts 不签发 Kubernetes +ServiceAccount token。 + +## 外部 workload attestation + +PVE、microVM、vsock metadata、Gitea runner 调度和云 metadata 不属于本仓库。对应系统 +以后可以输出本服务可验证的标准身份载体,例如 JWT-SVID 或已登记 issuer 的 JWT。 + +本服务不依赖或理解 VMID、vsock CID、Gitea job 环境变量、runner registration token +或虚拟机生命周期。外部系统负责防重放、实例代际和 attestation,本服务负责验证其最终 +credential 并执行签发策略。 + +## 部署边界 + +系统是无状态 HTTP 服务加数据库,可以部署在 Kubernetes、VM 或其他运行环境。部署位置 +不是信任语义的一部分;OAuth issuer 必须保持为稳定的 +`https://identity.ad.ddupan.top`。 + +workload-sts 依赖 OpenBao Transit 完成生产签名,但只使用自己的最小权限运行身份; +OpenBao 的人工管理与 break-glass 路径不得反向依赖 workload-sts。 + +## 文档入口 + +- 规范行为与验收标准:[`specification.md`](specification.md) +- 威胁模型与安全要求:[`security.md`](security.md) +- 首轮库与签名 PoC:[`poc.md`](poc.md) diff --git a/docs/poc.md b/docs/poc.md new file mode 100644 index 0000000..e981b19 --- /dev/null +++ b/docs/poc.md @@ -0,0 +1,114 @@ +# 首轮协议与签名 PoC + +| 项目 | 内容 | +| --- | --- | +| 状态 | Complete | +| 日期 | 2026-09-10 | + +本 PoC 为 v0.1 实现选择验证 HTTP/OAuth2 库边界,以及 OpenBao Transit RS256 能否生成 +由标准 JOSE 实现验证的 JWT/JWKS。 + +## OAuth2 框架 + +检查了 Fosite v0.49.0 的公开文档和 token endpoint 实现。Fosite 支持自定义 +`TokenEndpointHandler`,extension grant 可以通过 `CanSkipClientAuth` 跳过 client +authentication,但它没有现成的 RFC 8693 Token Exchange handler。 + +`NewAccessRequest` 仍会先执行 Fosite 的 client authentication/store lookup。v1 的 +`client_id` 是不注册、不认证的必填审计标签,因此要接入 Fosite 需要: + +- 自定义 RFC 8693 request/response handler; +- 绕开或适配 Fosite client store; +- 自定义 Transit-backed JWT strategy; +- 禁用本项目不提供的 authorize、refresh、OIDC、introspection 和 revocation 功能。 + +这套适配没有替项目减少核心安全逻辑,却引入了与 v1 不一致的 client/storage 模型。 +因此 v0.1 不采用 Fosite,也不把它加入运行依赖。 + +v0.1 使用: + +- Go `net/http` 与 `net/url` 处理窄 token endpoint; +- `chi/v5` 只承担路由和标准 `net/http` middleware 组合,不接管 OAuth 表单 binding; +- 项目代码执行 RFC 8693 子集的字段约束和 OAuth 错误映射; +- `go-jose/v4` 作为独立 JWS/JWK 互操作验证实现; +- 官方 `openbao/api/v2` 调用 Transit。 + +若未来加入完整 client registration、多个 grant 或 authorization endpoint,应重新评估 +Fosite 或独立 Authorization Server 产品。 + +HTTP 层比较了 chi 与 Echo。Echo 的集中错误处理和自动 binding 对通用 Web API 很方便, +但本项目只有少量固定端点,OAuth 表单又需要显式约束字段来源。选择 chi 可以直接暴露 +`http.Handler`、复用标准 middleware,并避免引入第二套 handler context 和未使用的 +binding/rendering 抽象。 + +HTTP server instrumentation 使用官方 `otelhttp`,并启用 OpenTelemetry Go trace/metric +SDK 的依赖注入边界。SDK 不记录 HTTP body;项目也不启用 header capture。OTLP exporter +类型留给进程启动配置选择,避免把 collector 的 gRPC/HTTP 拓扑固化进协议层。 +当前采用 OpenTelemetry Go SDK v1.46.0;`otelhttp` 随 contrib instrumentation 独立采用 +v0.71.0,仍属于其 experimental instrumentation version set,因此升级时需要单独检查 +HTTP semantic convention 与 metric 名称变化。 + +## Transit RS256 + +PoC 使用本地临时 OpenBao v2.6.1 dev server,创建不可导出的 `rsa-2048` Transit key, +验证以下链路: + +```text +读取 Transit key metadata + -> 固定 latest_version + -> kid = workload-sts-v + -> 构造 RS256 protected header 与 claims + -> 使用同一 key_version 请求 Transit 签名 + -> 从 Transit public_key 生成 JWKS + -> go-jose 解析 compact JWT + -> 使用 JWKS 中相同 kid 的 RSA 公钥验签 +``` + +测试结果为通过。Transit 调用使用: + +```text +path: transit/sign/workload-sts/sha2-256 +key type: rsa-2048 +signature_algorithm: pkcs1v15 +key_version: 显式指定 +input: base64(JWS signing input) +``` + +### PoC 发现 + +`kid` 必须在构造 JWS protected header 前确定,因此 signer 先读取 active Transit key +version,再用该 version 签名。不能使用“签名后从 `vault:vN:` 响应推导 `kid`”的流程, +因为修改 header 会改变 signing input。 + +签名响应为 `vault:vN:`。实现验证响应 version 与请求 version 一致, +并将原始 RSA signature 转为 JWS 使用的 Base64URL 编码。 + +JWKS 为 Transit metadata 中所有保留版本发布独立 key,`kid` 包含 key version,使轮换前 +签发且尚未过期的 JWT 仍可验证。 + +## 可执行验证 + +纯单元和互操作测试: + +```sh +make test +``` + +真实 Transit 集成测试需要一个仅用于测试、已经启用 `transit/` 并创建 +`transit/keys/workload-sts` RSA key 的 OpenBao: + +```sh +export WORKLOAD_STS_TEST_BAO_ADDR=http://127.0.0.1:8200 +export WORKLOAD_STS_TEST_BAO_TOKEN=test-only-token +make test-transit +``` + +集成测试只读取 key metadata 和请求签名,不创建、轮换或删除 Transit key。 + +## 尚未覆盖 + +- OpenBao JWT auth 对输出 JWT 的实际登录验证; +- kube-apiserver 外部 JWT authenticator 的实际验证; +- Transit key rotation 后新旧 `kid` 的端到端验证; +- workload-sts 自身通过 Kubernetes auth 获取最小权限 Transit token; +- HTTP token endpoint、TokenReview、binding/CEL 和 PostgreSQL policy version。 diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..f571048 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,166 @@ +# 安全模型 + +| 项目 | 内容 | +| --- | --- | +| 状态 | 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。 diff --git a/docs/specification.md b/docs/specification.md new file mode 100644 index 0000000..423beb3 --- /dev/null +++ b/docs/specification.md @@ -0,0 +1,506 @@ +# 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: +name: ci/homelab-infra-plan +``` + +输出 JWT 使用不可变 ID 作为 `sub`,可选的受控 claim 表示可读名称: + +```json +{ + "sub": "", + "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": "", + "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": "", + "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/)