SPIRE 系列之一:从 Workload Identity 到 Zero Trust
Posted on 四 23 4月 2026 in Journal
SPIRE 系列之一:从 Workload Identity 到 Zero Trust
零信任不是把所有服务都变成“不信任”,而是把“信任网络位置”改成“验证工作负载身份”。SPIFFE 定义身份标准,SPIRE 负责把这个身份发到每一个真实运行的进程手里。
系列导航: - 01:从 Workload Identity 到 Zero Trust(本文) - 02:SPIRE 架构深度解析 - 03:安全性分析与加固清单 - 04:实战 Lab:用零信任身份替代数据库密码分发
1. 先从一个老问题说起:服务到底凭什么互相信任?
咱们做微服务久了,很容易默认一件事:服务都在内网,应该差不到哪里去。可是真到排查权限问题、追一次凭证泄露、或者做跨集群调用时,才会发现这个“应该”其实很脆。
最常见的信任方式并不高级:
服务 A 调服务 B:
1. 看 IP:它来自内网,所以可信
2. 看网段:它在同一个 VPC,所以可信
3. 看 Token:它知道共享密钥,所以可信
4. 看证书:它拿着某个长期证书,所以可信
这些方式在小系统里能跑,甚至跑得还挺顺。可是系统一大,麻烦就会自己找上门。
IP 会漂移,Pod 会重建,节点会扩缩容,服务会跨集群迁移。更麻烦的是,密码、Token、长期证书这些东西,本身又成了新的 Secret。为了访问数据库,你给服务一个密码;为了访问 Vault,你又给服务一个 Vault Token;为了拿这个 Token,你还得给它另一个启动凭证。绕来绕去,最后还是回到那个经典问题:Secret Zero。
所谓 Secret Zero,就是系统启动时第一个秘密从哪里来。它像武侠小说里的“第一口真气”,解释不好,后面所有招式都站不住。
SPIFFE/SPIRE 要解决的,正是这个问题:
不要问:这个服务知道什么秘密?
而要问:这个进程到底是谁?它运行在哪里?是否满足预先登记的身份规则?
这就是 Workload Identity 的核心。它不靠“暗号”,而靠运行时证据。
2. Workload Identity 是什么?
先把几个词讲清楚。
| 概念 | 简单解释 | 在 SPIFFE/SPIRE 里的形态 |
|---|---|---|
| Workload | 一个正在运行的工作负载 | Pod、容器、进程、VM 上的服务 |
| Workload Identity | 工作负载的可验证身份 | spiffe://example.org/ns/prod/sa/payment |
| SPIFFE ID | 身份命名标准 | URI 格式,属于某个 trust domain |
| SVID | 身份凭证 | X.509-SVID 或 JWT-SVID |
| Trust Domain | 信任域 | 例如 example.org |
| SPIRE | SPIFFE 的参考实现 | 负责注册、签发、轮换、验证身份 |
SPIFFE 是标准,SPIRE 是实现。你可以把它们理解成:
SPIFFE = 身份证号码规则
SPIRE = 身份证签发机关 + 自动续期系统 + 身份核验系统
一个典型的 SPIFFE ID 长这样:
spiffe://example.org/ns/prod/sa/payment
它表达的不是“这个服务来自 10.0.1.23”,而是“这是 example.org 信任域里,prod 命名空间下,以 payment ServiceAccount 运行的工作负载”。
这比 IP 更接近业务语义,也更适合零信任授权。
3. Zero Trust 里的身份层应该长什么样?
零信任常被说成一句话:never trust, always verify。问题是,verify 什么?
如果验证的是 IP,那只是把传统网络边界换了个名字。如果验证的是静态 Token,那还是在相信一个可能泄露的字符串。如果验证的是工作负载身份,系统才真正开始变得可组合。
传统模型:
App A ── 内网 IP / 共享 Token ──▶ App B
Zero Trust 身份模型:
App A ── X.509-SVID / JWT-SVID ──▶ App B
│ │
└── SPIFFE ID 可验证 └── 根据 SPIFFE ID 做授权
SPIRE 在这个模型里承担三件事:
- 证明节点可信:Agent 启动时通过 Node Attestation 向 Server 证明自己。
- 证明进程可信:工作负载连接 Agent 时,Agent 通过内核、K8s、Docker 等证据识别它。
- 签发短期身份:Server 根据注册表签发 X.509-SVID 或 JWT-SVID,并由 Agent 缓存和轮换。
这三件事合在一起,就是“工作负载不用携带长期秘密,也能拿到短期可验证身份”。
4. SPIRE 的最小心智模型
先不用急着看配置。理解下面这张图,后面会轻松很多。
┌─────────────────────────────────────────────────────────────┐
│ SPIRE Server │
│ - Trust Domain / CA │
│ - Registration Entries │
│ - Node Attestation 验证 │
└───────────────────────┬─────────────────────────────────────┘
│ gRPC + mTLS
▼
┌─────────────────────────────────────────────────────────────┐
│ SPIRE Agent │
│ - 每个节点一个,通常以 DaemonSet 运行 │
│ - 通过 UDS 暴露 Workload API │
│ - 识别本机工作负载,缓存并轮换 SVID │
└───────────────────────┬─────────────────────────────────────┘
│ Unix Domain Socket
▼
┌─────────────────────────────────────────────────────────────┐
│ Workload │
│ - 调用 Workload API │
│ - 获取 X.509-SVID 或 JWT-SVID │
│ - 用身份做 mTLS、API 认证或访问控制 │
└─────────────────────────────────────────────────────────────┘
一句话:Server 管规则,Agent 看现场,Workload 拿身份。
5. 一个身份是怎么被发出来的?
SPIRE 不会凭空给每个进程发身份。它需要一张 Registration Entry,相当于“户口登记”。
spire-server entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/ns/prod/sa/payment \
-selector k8s:ns:prod \
-selector k8s:sa:payment \
-ttl 300
这条规则的意思是:
如果某个工作负载:
1. 运行在 node-1 对应的 Agent 下面
2. 属于 Kubernetes namespace prod
3. 使用 ServiceAccount payment
那么它可以获得:
spiffe://example.org/ns/prod/sa/payment
运行时流程大致如下:
T0: 管理员创建 Registration Entry
T1: Agent 完成 Node Attestation,获得节点身份
T2: Workload 连接 Agent 的 Unix Domain Socket
T3: Agent 通过 SO_PEERCRED / K8s API / cgroup 等证据识别 Workload
T4: Agent 将证据转换成 selectors,与 Registration Entry 匹配
T5: 匹配成功,Agent 从 Server 获取或从缓存返回 SVID
T6: Workload 使用 SVID 调用下游服务
注意这里有个关键点:工作负载没有带密码来证明自己。它只是“作为那个进程”运行在那里,由操作系统和平台提供证据。
6. X.509-SVID 和 JWT-SVID 怎么选?
SPIRE 支持两类 SVID:
| 维度 | X.509-SVID | JWT-SVID |
|---|---|---|
| 适合场景 | 服务间 mTLS | HTTP/API 调用、网关、跨语言系统 |
| 传输方式 | TLS 握手自动使用证书 | Authorization: Bearer <token> |
| 身份位置 | 证书 SAN 中的 SPIFFE ID | JWT sub claim 中的 SPIFFE ID |
| 授权方式 | 校验证书链 + SPIFFE ID | 校验签名、过期时间、audience、SPIFFE ID |
| 轮换模型 | 流式更新,应用可无感 | 按需获取,短 TTL |
我通常这样选:
服务之间能做 mTLS:优先 X.509-SVID
已有 HTTP Bearer Token 体系:用 JWT-SVID 过渡
跨信任域调用:两者都可以,关键是验证 trust bundle / JWKS
网关到后端:JWT-SVID 更容易接入现有中间件
不要把这两者看成谁替代谁。X.509-SVID 更像“连接级身份”,JWT-SVID 更像“请求级身份”。一个成熟系统里,两者很可能同时存在。
7. 落地 SPIRE 的四步路线
如果你准备把 SPIRE 用到生产系统里,不建议一上来就全量替换所有服务认证。更稳妥的路线是四步。
7.1 第一步:定义 Trust Domain 和身份命名规范
先决定身份长什么样。比如:
spiffe://example.org/ns/{namespace}/sa/{service_account}
spiffe://example.org/service/{service_name}
spiffe://example.org/team/{team}/service/{service}
命名规范越早定越好。SPIFFE ID 后面会进入授权策略、审计日志、告警规则、服务依赖图。乱命名的代价会慢慢变大。
7.2 第二步:从一个非核心链路开始
选一个低风险服务,先做身份签发和验证,不急着改所有权限模型。
目标不是第一天就“零信任全覆盖”,
而是先证明:
1. Workload 能稳定拿到 SVID
2. 下游能验证 SPIFFE ID
3. 证书/Token 能自动轮换
4. 监控能看见异常
7.3 第三步:把授权从“凭证存在”升级到“身份匹配”
传统认证常常只问:“Token 合法吗?”
SPIFFE/SPIRE 应该进一步问:“这个 SPIFFE ID 是否有权访问这个资源?”
ALLOWED_SPIFFE_IDS = {
"spiffe://example.org/ns/prod/sa/order-service",
}
if spiffe_id not in ALLOWED_SPIFFE_IDS:
raise PermissionError("unauthorized workload identity")
这一步才真正把身份变成访问控制。
7.4 第四步:逐步替换静态 Secret
最适合优先替换的是这些场景:
| 场景 | 为什么适合 |
|---|---|
| 服务间 mTLS | X.509-SVID 天然匹配 |
| 内部 API 调用 | JWT-SVID 易接入 |
| 数据库密码代理 | 可以消除应用侧长期密码 |
| CI/CD 部署任务 | 工作负载身份比共享 Token 更可审计 |
| 跨集群服务调用 | Federation 可以统一信任模型 |
第 4 篇 Lab 会演示一个具体例子:订单服务启动时没有数据库密码,只用 SPIFFE 身份向 Secret Server 换取短期数据库访问能力。
8. 部署模式:DaemonSet 还是 Sidecar?
在 Kubernetes 里,SPIRE Agent 常见两种部署方式。
8.1 DaemonSet 模式(大多数生产环境首选)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: spire-agent
spec:
template:
spec:
containers:
- name: spire-agent
image: ghcr.io/spiffe/spire-agent:1.9.x
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
volumes:
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: DirectoryOrCreate
每个节点一个 Agent,多个 Pod 共享。优点是资源效率高,运维集中;代价是安全边界是节点级。
8.2 Sidecar 模式(强隔离场景)
containers:
- name: spire-agent
image: ghcr.io/spiffe/spire-agent:1.9.x
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
- name: my-app
image: my-app:latest
每个 Pod 一个 Agent。隔离性更强,但每个 Pod 都多一份资源开销。除非你有强多租户隔离要求,否则不要默认走 Sidecar。
| 维度 | DaemonSet | Sidecar |
|---|---|---|
| 资源效率 | 高,节点级共享 | 低,每 Pod 一份 |
| 运维复杂度 | 低 | 高 |
| 隔离性 | 节点级 | Pod 级 |
| 适用场景 | 大多数生产集群 | 高安全、多租户、特殊隔离需求 |
9. 资源成本:别因为“多一个 Agent”就误判
采纳 SPIRE 时,很多人的第一个问题是:它会不会拖慢应用?
先给一个保守估算:
| 资源 | 常见范围 | 说明 |
|---|---|---|
| 内存 | 30-80 MB | Go runtime、gRPC 连接、SVID 缓存、attestor 缓存 |
| CPU 空闲 | 近似 0 | 事件驱动,没有请求时基本不干活 |
| CPU 活跃 | 通常 < 50m | 取决于 SVID 请求频率和缓存命中率 |
| JWT 缓存命中延迟 | < 1ms | 本地 UDS + 内存缓存 |
| JWT 缓存未命中延迟 | 5-50ms | 需要 Agent 到 Server 请求签发 |
| 日常网络 | 很低 | Agent 与 Server 主要在签发/轮换时通信 |
JWT-SVID 的请求路径可以这样理解:
应用 ──▶ SPIRE Agent ──▶ SPIRE Server
│
├─ 缓存命中:直接返回,通常 < 1ms
└─ 缓存未命中:请求 Server 签发,通常 5-50ms
同一个 SPIFFE ID 加同一个 audience 的 JWT-SVID 会被缓存。也就是说,只要应用不是每次都换 audience,大多数请求都会走本地缓存。
大规模场景下,部署方式的差异更明显:
DaemonSet 模式:
100 个节点 = 100 个 Agent
总额外资源约:5 CPU cores + 8 GB RAM 级别
Sidecar 模式:
1000 个 Pod = 1000 个 Agent
总额外资源约:20 CPU cores + 40 GB RAM 级别
所以资源优化的第一条不是调参数,而是选对模式:大多数场景优先 DaemonSet。
10. 监控要看什么?
不要只看 Agent 是否存活。SPIRE 的可观测性应该围绕身份签发和轮换来做。
# SPIRE Agent 关键指标示例
- spire_agent_cache_manager_jwt_svid_cache_hit_total
- spire_agent_cache_manager_jwt_svid_cache_miss_total
- spire_agent_cache_manager_svid_cache_size
- spire_agent_workload_api_duration_seconds
- spire_agent_node_attestor_duration_seconds
告警建议:
# JWT 缓存命中率过低
- alert: SpireAgentLowCacheHitRate
expr: |
rate(spire_agent_cache_manager_jwt_svid_cache_hit_total[5m]) /
(rate(spire_agent_cache_manager_jwt_svid_cache_hit_total[5m]) +
rate(spire_agent_cache_manager_jwt_svid_cache_miss_total[5m])) < 0.8
for: 10m
annotations:
summary: "SPIRE Agent JWT 缓存命中率低于 80%"
# Agent 内存异常
- alert: SpireAgentHighMemory
expr: container_memory_usage_bytes{container="spire-agent"} > 120 * 1024 * 1024
for: 5m
annotations:
summary: "SPIRE Agent 内存使用超过 120MB"
11. 常见误区
11.1 “有了 SPIRE 就不用授权了”
错。SPIRE 解决的是身份签发与验证,不自动决定谁能访问什么。授权策略仍然要写,只是策略对象从 IP、Token、用户名,变成了 SPIFFE ID。
11.2 “SPIFFE ID 越细越好”
也不一定。身份太粗,权限边界不清;身份太细,注册表和策略会爆炸。比较实用的粒度是:服务、命名空间、ServiceAccount、环境。
11.3 “Join Token 可以上生产”
Join Token 适合开发、测试和一次性 bootstrap。生产环境尽量用云厂商或 Kubernetes 原生 attestor,例如 aws_iid、gcp_iit、azure_msi、k8s_psat。
11.4 “DaemonSet 模式不安全”
不准确。DaemonSet 模式的边界是节点,不是 Pod。它适合大多数普通生产环境。高安全多租户场景可以考虑 Sidecar、CSI Driver、更严格的 selector 和节点隔离。
12. 落地检查清单
设计阶段:
[ ] 定义 trust domain
[ ] 定义 SPIFFE ID 命名规范
[ ] 决定 X.509-SVID / JWT-SVID 使用边界
[ ] 确定首个试点服务
部署阶段:
[ ] Server 使用生产级 datastore,例如 PostgreSQL
[ ] Server CA 密钥接入 KMS/HSM 或至少做好磁盘保护
[ ] Agent 优先使用 DaemonSet
[ ] Kubernetes 环境优先使用 k8s_psat
[ ] Workload selector 避免过宽,例如只用 unix:uid:0
应用阶段:
[ ] 应用使用 Workload API 获取 SVID
[ ] 下游验证 SPIFFE ID,而不只是验证证书/Token 合法
[ ] 授权策略基于 SPIFFE ID 编写
[ ] 日志不打印 JWT-SVID 或私钥材料
运维阶段:
[ ] 监控 SVID 签发延迟和缓存命中率
[ ] 监控 Server/Agent 健康状态
[ ] 演练证书泄露、CA 轮换、Agent 驱逐
[ ] 定期审计 Registration Entries
13. 小结
SPIFFE/SPIRE 的价值,不在于“又引入了一套证书系统”。它真正改变的是信任模型:
从:谁在内网,谁知道密码
到:谁能证明自己的工作负载身份,谁才被授权访问
这就是 Workload Identity 对 Zero Trust 的意义。
第一篇先把概念、落地路线和资源成本讲清楚。下一篇我们往下走一层,拆 SPIRE 的 Server、Agent、Workload API、Registration Entry 和插件体系。
一句话,先别急着上 YAML。先把身份模型想明白。否则配置写得再热闹,也只是“照着 YAML 拜神”。
下一篇:SPIRE 系列之二:架构深度解析 — Server、Agent、Workload API 与 Registration Entry 的完整剖析。