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

21 KiB
Raw Permalink Blame History

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 为:

https://identity.ad.ddupan.top

所有 metadata、JWKS 和 JWT iss 必须使用完全一致的 issuer。服务部署地址、Pod IP、 内部 Service DNS 和数据库位置不能进入 iss。

5.2 Principal

每个 principal 具有不可变 ID 和可读名称:

id:   <immutable identifier>
name: ci/homelab-infra-plan

输出 JWT 使用不可变 ID 作为 sub,可选的受控 claim 表示可读名称:

{
  "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 为:

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,只发布:

/.well-known/oauth-authorization-server
/oauth2/token
/oauth2/jwks
/healthz
/readyz

不发布 authorize、OIDC discovery、introspection 或 revocation endpoint。

6.2 Token Exchange 请求

服务必须接受表单编码的 RFC 8693 请求:

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 小写字母、数字和中横线,且首尾必须是字母 或数字。规范正则表达式为:

^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$

6.3 成功响应

成功响应使用标准 OAuth2 token response:

{
  "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 使用以下输入匹配上游身份:

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 映射示例:

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 至少关联:

audience
allowed scopes
token profile
optional CEL condition

有效授权为已匹配 binding、issuance rule 和客户端请求的交集。客户端可以请求更少, 不能请求更多。服务端直接使用唯一匹配 rule 绑定的 token profile;零个匹配表示不允许 签发,多个匹配表示配置冲突并失败关闭。

9.2 CEL

项目使用现成 CEL 实现,不自行编写表达式解析器。CEL 只处理基于已验证事实的条件, 不能替代 principal、audience 和 scope 的结构化模型。

初始 CEL 环境计划提供:

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 必须包含:

{
  "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 至少产生:

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;未配置 telemetry 时纯 chi handler 不创建 OTel instrumentation。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. 规范性参考