文档:记录 SPIRE 与 OpenBao workload identity 用法
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
SPIRE 是 homelab 的机器与 workload identity 根。人类身份继续由 Samba AD 与
|
||||
Authelia 提供;SPIRE 不替代人类 OIDC,也不承担目标服务的资源授权。
|
||||
|
||||
部署状态、workload 接入、JWT-SVID → OpenBao exchange、安全规则和故障恢复详见
|
||||
[RUNBOOK.md](RUNBOOK.md)。本文只保留部署声明与关键恢复边界。
|
||||
|
||||
## 部署范围
|
||||
|
||||
Flux 安装 SPIFFE hardened charts:
|
||||
@@ -73,8 +76,9 @@ sudo k3s kubectl -n spire-system get daemonset,pods
|
||||
必须先确认 `spire-crds` Ready,随后 `spire` Ready。SPIRE Server 应连接 PostgreSQL,
|
||||
Agent 应通过 PSAT attestation 注册,CSI Driver 应在节点 Ready。
|
||||
|
||||
首个业务验收另行增加一个专用测试 Pod 与 `ClusterSPIFFEID`,验证取得
|
||||
`aud=openbao` 的 JWT-SVID 后登录 OpenBao。PoC 完成前不修改生产认证方式。
|
||||
2026-09-14 已使用临时测试 Pod 与 `ClusterSPIFFEID` 完成
|
||||
`aud=openbao` JWT-SVID → OpenBao 登录、token 自省和主动吊销的端到端验收;临时
|
||||
Kubernetes 资源与 registration entries 已清理。
|
||||
|
||||
OpenBao 中对应的 Terraform 资源位于
|
||||
`../../infrastructure/openbao/terraform/auth-spire.tf`。PoC role 只接受精确 subject
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
# SPIRE 与 OpenBao workload identity runbook
|
||||
|
||||
本文记录 homelab 中 workload 如何取得 SPIFFE 身份、如何把 JWT-SVID 交换成
|
||||
OpenBao 短期 token,以及相关的部署、接入、验证和恢复操作。这里的命令默认在
|
||||
`laptop` 上执行。
|
||||
|
||||
## 1. 当前架构
|
||||
|
||||
```text
|
||||
Kubernetes Pod
|
||||
│ Pod ServiceAccount + Pod UID
|
||||
▼
|
||||
SPIRE Agent(每节点 DaemonSet,k8s workload attestor)
|
||||
│ Unix socket: /spiffe-workload-api/spire-agent.sock
|
||||
│ Agent 自身通过 k8s_psat 向 Server 证明节点身份
|
||||
▼
|
||||
SPIRE Server(trust domain: ddupan.top)
|
||||
├─ registration state → shared PostgreSQL
|
||||
├─ signing keys → localpv-zfs-ceph PVC
|
||||
└─ JWT public keys → OIDC Discovery Provider
|
||||
│
|
||||
▼
|
||||
https://spire-oidc.ad.ddupan.top
|
||||
│ discovery + JWKS
|
||||
▼
|
||||
OpenBao auth/jwt-spire/login
|
||||
│ exact sub + aud + role
|
||||
▼
|
||||
短期、最小权限 Bao token
|
||||
```
|
||||
|
||||
各层职责必须保持分离:
|
||||
|
||||
- Kubernetes ServiceAccount 是 Pod 的初始身份证明,不是跨平台 IAM token;
|
||||
- SPIRE 负责 workload 身份、证明和 SVID 签发,不保存业务 secret;
|
||||
- OpenBao 验证 JWT-SVID,并把身份映射为本地 policy;
|
||||
- 目标服务最终仍负责自己的资源授权,SPIFFE ID 本身不等于权限;
|
||||
- 人类身份继续使用 Samba AD + Authelia OIDC,不经过 `jwt-spire`。
|
||||
|
||||
## 2. 线上对象与稳定标识
|
||||
|
||||
| 项目 | 当前值 |
|
||||
|---|---|
|
||||
| SPIRE chart | `0.30.2` |
|
||||
| SPIRE | `1.15.3` |
|
||||
| SPIRE CRDs chart | `0.6.1` |
|
||||
| trust domain | `ddupan.top` |
|
||||
| Kubernetes cluster name | `homelab` |
|
||||
| Controller Manager class | `spire-mgmt-spire` |
|
||||
| JWT issuer | `https://spire-oidc.ad.ddupan.top` |
|
||||
| OpenBao auth mount | `jwt-spire` |
|
||||
| OpenBao login endpoint | `auth/jwt-spire/login` |
|
||||
| SPIRE Server namespace | `spire-server` |
|
||||
| Agent/CSI namespace | `spire-system` |
|
||||
| Helm management namespace | `spire-mgmt` |
|
||||
|
||||
`trustDomain`、`clusterName`、issuer URL 与 Controller Manager class 都进入身份或
|
||||
下游信任配置。修改它们不是普通 rename,必须按 trust-domain migration 处理。
|
||||
|
||||
## 3. 身份与授权模型
|
||||
|
||||
Kubernetes workload 的默认 SPIFFE ID 约定为:
|
||||
|
||||
```text
|
||||
spiffe://ddupan.top/ns/<namespace>/sa/<service-account>
|
||||
```
|
||||
|
||||
身份必须同时在两侧声明:
|
||||
|
||||
1. SPIRE `ClusterSPIFFEID` 决定哪些 Pod 可以取得该身份;
|
||||
2. OpenBao JWT role 决定该 `sub`、`aud` 能换取哪些 policy。
|
||||
|
||||
这两个声明是有意的双重门:只有 SPIRE entry 而没有 Bao role 时,workload 能取得
|
||||
SVID,但不能登录 Bao;只有 Bao role 而没有 SPIRE entry 时,没有 workload 能铸造
|
||||
满足条件的 JWT。
|
||||
|
||||
禁止使用以下宽泛规则:
|
||||
|
||||
- 给所有 Pod 启用 fallback `ClusterSPIFFEID`;
|
||||
- OpenBao role 接受整个 `spiffe://ddupan.top/*`;
|
||||
- 仅按 namespace 匹配高权限身份,却不限制 ServiceAccount 和 Pod labels;
|
||||
- 多个安全边界不同的 workload 共用同一个 ServiceAccount;
|
||||
- 给 workload token 附带 `default` 或 `admin` policy。
|
||||
|
||||
## 4. 新 workload 接入流程
|
||||
|
||||
以下示例为 namespace `example` 中的 ServiceAccount `example-worker`。
|
||||
|
||||
### 4.1 创建专用 ServiceAccount
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: example-worker
|
||||
namespace: example
|
||||
```
|
||||
|
||||
不要使用 namespace 的 `default` ServiceAccount。
|
||||
|
||||
### 4.2 声明 ClusterSPIFFEID
|
||||
|
||||
```yaml
|
||||
apiVersion: spire.spiffe.io/v1alpha1
|
||||
kind: ClusterSPIFFEID
|
||||
metadata:
|
||||
name: example-worker
|
||||
labels:
|
||||
spire.spiffe.io/class-name: spire-mgmt-spire
|
||||
spec:
|
||||
className: spire-mgmt-spire
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: example
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: example-worker
|
||||
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
|
||||
```
|
||||
|
||||
Controller Manager 最终会为具体 Pod UID 创建 registration entry。检查状态:
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl get clusterspiffeid example-worker -o yaml
|
||||
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
|
||||
/opt/spire/bin/spire-server entry show \
|
||||
-spiffeID spiffe://ddupan.top/ns/example/sa/example-worker
|
||||
```
|
||||
|
||||
`status.stats.entryFailures` 必须是 `0`。Pod 重建后 UID 会变化,短暂看到旧 entry
|
||||
属于正常收敛过程。
|
||||
|
||||
### 4.3 挂载 Workload API
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
serviceAccountName: example-worker
|
||||
containers:
|
||||
- name: worker
|
||||
volumeMounts:
|
||||
- name: spiffe-workload-api
|
||||
mountPath: /spiffe-workload-api
|
||||
readOnly: true
|
||||
env:
|
||||
- name: SPIFFE_ENDPOINT_SOCKET
|
||||
value: unix:///spiffe-workload-api/spire-agent.sock
|
||||
volumes:
|
||||
- name: spiffe-workload-api
|
||||
csi:
|
||||
driver: csi.spiffe.io
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
挂载 socket 不会自动获得身份。Agent 会对调用进程执行 workload attestation,只有
|
||||
selector 命中 registration entry 才签发 SVID。
|
||||
|
||||
应用应优先使用 SPIFFE SDK,通过 Workload API 按需取得并自动轮换 SVID。不要把
|
||||
JWT-SVID 写入 Kubernetes Secret、镜像、持久卷、CI artifact 或日志。
|
||||
|
||||
### 4.4 声明 OpenBao policy
|
||||
|
||||
在 `infrastructure/openbao/terraform/policies/` 中为 workload 建独立 policy。例如:
|
||||
|
||||
```hcl
|
||||
path "kv/data/apps/example/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "auth/token/revoke-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
```
|
||||
|
||||
不要直接复用 `admin`。动态数据库凭据、SSH 签名和 KV 应分别授权到精确 path。
|
||||
|
||||
### 4.5 声明 OpenBao JWT role
|
||||
|
||||
在 `infrastructure/openbao/terraform/auth-spire.tf` 增加 role:
|
||||
|
||||
```hcl
|
||||
resource "vault_jwt_auth_backend_role" "example_worker" {
|
||||
backend = vault_jwt_auth_backend.spire.path
|
||||
role_name = "example-worker"
|
||||
role_type = "jwt"
|
||||
|
||||
user_claim = "sub"
|
||||
bound_audiences = ["openbao"]
|
||||
bound_claims = {
|
||||
sub = "spiffe://ddupan.top/ns/example/sa/example-worker"
|
||||
}
|
||||
|
||||
token_policies = [vault_policy.example_worker.name]
|
||||
token_no_default_policy = true
|
||||
token_ttl = 300
|
||||
token_max_ttl = 900
|
||||
}
|
||||
```
|
||||
|
||||
同一个 JWT 可以请求多个 audience,但 Bao role 只接受 `openbao`。未来接入其他服务时
|
||||
应给对应服务使用独立 audience,不能把 `openbao` 当作通用 audience。
|
||||
|
||||
Terraform apply 必须遵循 `infrastructure/openbao/README.md` 的 remote-state 和认证
|
||||
流程。任何包含 destroy/replace 的 plan 都应停止审查;JWT role/policy 的正常新增应为
|
||||
纯 `add`。
|
||||
|
||||
## 5. JWT-SVID 交换流程
|
||||
|
||||
逻辑请求如下:
|
||||
|
||||
```http
|
||||
POST /v1/auth/jwt-spire/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"role": "example-worker",
|
||||
"jwt": "<aud=openbao 的 JWT-SVID>"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应中的 `auth.client_token` 是短期 Bao token。它只应存在于进程内存或
|
||||
job-scoped `tmpfs`;通常无需主动续期,过期前重新用 Workload API 获取 JWT-SVID 并
|
||||
登录即可。
|
||||
|
||||
如果使用 SPIRE CLI 调试,`1.15.3` 的 JSON 输出顶层是数组,JWT 位于:
|
||||
|
||||
```text
|
||||
.[0].svids[0].svid
|
||||
```
|
||||
|
||||
调试脚本必须把 JSON 捕获到变量中,禁止直接输出:
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
|
||||
JWT_RESPONSE="$(spire-agent api fetch jwt \
|
||||
-audience openbao \
|
||||
-socketPath /spiffe-workload-api/spire-agent.sock \
|
||||
-output json)"
|
||||
JWT_SVID="$(printf '%s' "$JWT_RESPONSE" | jq -er '.[0].svids[0].svid')"
|
||||
|
||||
LOGIN_PAYLOAD="$(jq -nc \
|
||||
--arg role example-worker \
|
||||
--arg jwt "$JWT_SVID" \
|
||||
'{role:$role,jwt:$jwt}')"
|
||||
LOGIN_RESPONSE="$(curl --fail-with-body --silent --show-error \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$LOGIN_PAYLOAD" \
|
||||
https://bao.ad.ddupan.top:8200/v1/auth/jwt-spire/login)"
|
||||
|
||||
export BAO_ADDR=https://bao.ad.ddupan.top:8200
|
||||
export BAO_TOKEN="$(printf '%s' "$LOGIN_RESPONSE" | jq -er '.auth.client_token')"
|
||||
```
|
||||
|
||||
不要在 shell 中启用 `set -x`,不要 `echo "$JWT_SVID"` 或输出完整 login response。
|
||||
cleanup 阶段可以尽力主动吊销;短 TTL 仍是主要安全边界:
|
||||
|
||||
```bash
|
||||
bao token revoke -self
|
||||
unset BAO_TOKEN JWT_SVID JWT_RESPONSE LOGIN_RESPONSE LOGIN_PAYLOAD
|
||||
```
|
||||
|
||||
## 6. CI 与 AI Agent 使用方式
|
||||
|
||||
CI job/Agent 不应接收长期 `BAO_TOKEN`。标准启动顺序是:
|
||||
|
||||
1. 调度到带 SPIRE Agent 与 CSI Driver 的节点;
|
||||
2. 以专用 ServiceAccount 启动,挂载 Workload API socket;
|
||||
3. 获取目标 audience 的 JWT-SVID;
|
||||
4. 用对应 Bao role 换取短期 token;
|
||||
5. 在同一进程树中以环境变量调用 `tofu`、Ansible 或其他工具;
|
||||
6. cleanup 尝试 `revoke-self`,随后销毁 job/VM/容器。
|
||||
|
||||
通用 credential-exec 包装器未来应负责步骤 3–6。它必须满足:
|
||||
|
||||
- 不把 JWT-SVID 或 Bao token 写到 stdout/stderr;
|
||||
- 不把凭据传入命令行参数,避免出现在进程列表;
|
||||
- 子进程退出后清除环境和临时文件;
|
||||
- 不尝试把短期 token 上传到 Actions Secret 或 artifact;
|
||||
- role、audience 和目标命令由受审查的 pipeline 配置决定。
|
||||
|
||||
Kubernetes 以外的执行环境不能伪造 ServiceAccount。未来应分别使用 host SPIRE
|
||||
Agent、TPM/DevID、cloud instance identity、GitHub OIDC 等初始证明接入同一信任模型。
|
||||
|
||||
## 7. 日常检查
|
||||
|
||||
### Flux 与 Helm
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl -n flux-system get kustomization spire
|
||||
sudo k3s kubectl -n spire-mgmt get helmrepository,helmrelease
|
||||
```
|
||||
|
||||
### Server、Agent 与 CSI
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl -n spire-server get pods,pvc
|
||||
sudo k3s kubectl -n spire-system get daemonset,pods
|
||||
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
|
||||
/opt/spire/bin/spire-server agent list
|
||||
```
|
||||
|
||||
Agent 应显示 `Attestation type: k8s_psat` 与 `Can re-attest: true`。
|
||||
|
||||
### OIDC discovery 与 JWKS
|
||||
|
||||
```bash
|
||||
dig @192.168.10.5 spire-oidc.ad.ddupan.top A +short
|
||||
dig @192.168.10.127 spire-oidc.ad.ddupan.top A +short
|
||||
curl --fail --silent \
|
||||
https://spire-oidc.ad.ddupan.top/.well-known/openid-configuration | jq
|
||||
curl --fail --silent https://spire-oidc.ad.ddupan.top/keys | jq '.keys | length'
|
||||
```
|
||||
|
||||
discovery 的 `issuer` 必须严格等于
|
||||
`https://spire-oidc.ad.ddupan.top`。路径、scheme、hostname 或尾部 `/` 的差异都会
|
||||
导致 JWT 验证失败。JWKS 是公开验证材料,不是 secret。
|
||||
|
||||
### OpenBao
|
||||
|
||||
管理员只检查非敏感配置:
|
||||
|
||||
```bash
|
||||
export BAO_ADDR=https://bao.ad.ddupan.top:8200
|
||||
bao auth list
|
||||
bao read auth/jwt-spire/role/<role-name>
|
||||
bao policy read <policy-name>
|
||||
```
|
||||
|
||||
## 8. 故障排查
|
||||
|
||||
### `no identity issued`
|
||||
|
||||
依次检查:
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl get clusterspiffeid <name> -o yaml
|
||||
sudo k3s kubectl -n <namespace> get pod <pod> \
|
||||
-o custom-columns=NAME:.metadata.name,UID:.metadata.uid,SA:.spec.serviceAccountName,LABELS:.metadata.labels
|
||||
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
|
||||
/opt/spire/bin/spire-server entry show -spiffeID <expected-spiffe-id>
|
||||
```
|
||||
|
||||
确认 namespace、ServiceAccount、Pod labels 与 entry 中的 `k8s:pod-uid`。刚启动的
|
||||
一次性 Job 可能在 Controller Manager 建 entry 前请求身份并失败;生产客户端应重试
|
||||
Workload API,而不是假设 Pod 一启动身份就已可用。
|
||||
|
||||
### `auth/jwt-spire/login` 返回 400
|
||||
|
||||
常见原因:
|
||||
|
||||
- JWT audience 不是 role 的 `bound_audiences`;
|
||||
- JWT `sub` 与 role 的 `bound_claims.sub` 不完全一致;
|
||||
- issuer 与 `bound_issuer` 不一致;
|
||||
- Bao 无法解析或验证 `spire-oidc.ad.ddupan.top`;
|
||||
- issuer/JWKS route 被 Authelia forward-auth 拦截;
|
||||
- SVID 已过期,或者节点与 Bao 时钟偏差过大;
|
||||
- 脚本错误解析 CLI JSON,向 Bao 发送了空 JWT。
|
||||
|
||||
排查时只解码 JWT header/claims,禁止记录原始 token。公开 endpoint 可单独验证:
|
||||
|
||||
```bash
|
||||
curl --fail https://spire-oidc.ad.ddupan.top/.well-known/openid-configuration
|
||||
curl --fail https://spire-oidc.ad.ddupan.top/keys
|
||||
```
|
||||
|
||||
### CSI mount 失败
|
||||
|
||||
如果事件包含 `driver name csi.spiffe.io not found`:
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl get csidriver csi.spiffe.io
|
||||
sudo k3s kubectl -n spire-system get daemonset spire-spiffe-csi-driver
|
||||
sudo k3s kubectl -n spire-system logs daemonset/spire-spiffe-csi-driver \
|
||||
-c spiffe-csi-driver --tail=100
|
||||
```
|
||||
|
||||
首次安装时 OIDC Provider/业务 Pod 可能早于 CSI registration,短暂 mount retry 正常;
|
||||
持续失败才需要处理。
|
||||
|
||||
### Server 无法连接 PostgreSQL
|
||||
|
||||
```bash
|
||||
sudo k3s kubectl -n shared-db get cluster shared-postgresql
|
||||
sudo k3s kubectl -n spire-server get secret spire-postgresql \
|
||||
-o go-template='{{range $k, $_ := .data}}{{$k}}{{"\n"}}{{end}}'
|
||||
sudo k3s kubectl -n spire-server logs statefulset/spire-server \
|
||||
-c spire-server --tail=100
|
||||
```
|
||||
|
||||
Secret 必须有 `password` key。禁止为排障直接输出或提交其值。
|
||||
|
||||
## 9. 轮换、备份与恢复
|
||||
|
||||
SPIRE 会自动轮换 X.509 CA 与 JWT signing key;OIDC Provider 从 Workload API 获取
|
||||
当前 JWKS,下游按 `kid` 验证。正常轮换不应要求更新 Bao role。
|
||||
|
||||
必须备份两类不同状态:
|
||||
|
||||
- PostgreSQL:registration entries、agent state 和 SPIRE metadata;
|
||||
- `spire-data-spire-server-0` PVC:disk KeyManager 的 trust-domain signing keys。
|
||||
|
||||
仅恢复 PostgreSQL 而丢失 PVC,不等于恢复 SPIRE。签名密钥丢失会使既有 SVID/JWKS
|
||||
信任链失效。恢复顺序:
|
||||
|
||||
1. 恢复共享 PostgreSQL;
|
||||
2. 恢复 signing-key PVC;
|
||||
3. 启动 SPIRE Server;
|
||||
4. 确认 bundle/JWKS 后启动或恢复 Agent;
|
||||
5. 最后恢复依赖 SPIRE 登录 Bao 的 workload。
|
||||
|
||||
不要以空数据库或空 PVC“修复”启动失败。若确实需要重建 trust domain,应把它作为
|
||||
全体下游重新建立信任的灾难恢复事件处理。
|
||||
|
||||
## 10. 当前 PoC 结论与后续工作
|
||||
|
||||
2026-09-14 已完成并清理一次临时 PoC:
|
||||
|
||||
- workload 取得 `aud=openbao` JWT-SVID;
|
||||
- 精确 subject 成功登录 `jwt-spire/spire-poc`;
|
||||
- 返回 token 仅含 `spire-poc` policy,TTL 为 300 秒,无 default policy;
|
||||
- `lookup-self` 成功,随后 `revoke-self` 并验证 token 已失效;
|
||||
- 临时 Namespace、Pod/Job、ServiceAccount、`ClusterSPIFFEID` 与 registration entries
|
||||
均已删除;
|
||||
- Terraform 完整 plan 最终为 `No changes`。
|
||||
|
||||
下一步不是重复 PoC,而是为真实 CI/AI Agent 定义:
|
||||
|
||||
- 独立 ServiceAccount 和稳定 SPIFFE ID;
|
||||
- 按能力拆分的 OpenBao policy(例如 SSH CA、S3 state、数据库动态凭据);
|
||||
- 通用 credential-exec 包装器;
|
||||
- token 获取失败、过期与 cleanup 的客户端重试语义;
|
||||
- Kubernetes 外 host/VM/microVM 的 SPIRE Agent attestation 方案。
|
||||
Reference in New Issue
Block a user