为什么要搞工作负载身份#
在"零信任"这个词被过度营销之前,我对它的第一反应是"又是一个新瓶装旧酒的词"。真正让我改观的一次事故是 2023 年的某次内网穿透演练:攻击者拿到一台运维跳板机的 SSH 密钥,通过密钥连上 VPN,然后在内网里畅通无阻地访问了几十个微服务,因为那些服务之间互相信任 VPC 内网 IP。整个事件里没有任何一个身份校验环节,大家都在信"你从内网来"。
工作负载身份(Workload Identity)要解决的就是这个问题:让每一个服务、每一个进程、每一个 Pod 都有一个可验证、可撤销、短生命周期的身份凭据,服务之间互相调用必须双向验证身份,而不是信任 IP/网段/机器。SPIFFE(Secure Production Identity Framework For Everyone)是 CNCF 毕业的一套身份标准,SPIRE 是这套标准的参考实现,也是目前最成熟的开源实现,被 Uber、Bloomberg、Square、Netflix 等大规模部署。
这篇文章我会从 SPIFFE 的核心概念讲起,然后用一个真实的"Kubernetes + 虚拟机混合部署"场景把 SPIRE 从零部署一遍,最后讲我们在生产运营 SPIRE 两年多踩过的所有坑。本文基于 SPIRE 1.10+(2025 年下半年版本)。
一、核心概念:SPIFFE ID、SVID、信任域#
1.1 SPIFFE ID#
SPIFFE ID 是一个长得像 URI 的字符串,格式:
spiffe://<trust-domain>/<path>
举例:
spiffe://prod.example.com/ns/payments/sa/checkout-service
spiffe://prod.example.com/vm/db-proxy/region/us-west-2
spiffe://prod.example.com/ci/runner/pipeline/12345
它的作用是唯一标识一个工作负载。信任域(trust domain)是一个组织边界,类似 Kerberos 的 Realm 或者 X.509 的 CA。一个工作负载只属于一个信任域,跨信任域通信需要"联邦"(federation)。
关键设计哲学:SPIFFE ID 不是给人看的,是给机器看的。它不携带授权信息(是不是 admin、有没有 read 权限),只携带身份。授权是上层的事情(比如 OPA、Istio AuthorizationPolicy)。
1.2 SVID:身份的可验证载体#
SVID (SPIFFE Verifiable Identity Document) 是 SPIFFE ID 的可验证形式,有两种:
- X.509-SVID:一张 X.509 证书,SPIFFE ID 放在 SAN URI 字段里,用于 mTLS
- JWT-SVID:一个 JWT,SPIFFE ID 放在
sub字段里,用于 HTTP Authorization 头
两者各有场景:mTLS 走 X.509,REST API 和 Webhook 一般走 JWT。生产里我们两种都用,X.509 用得更多。
重点:SVID 的生命周期非常短,默认 1 小时,可以配置到 5 分钟。短生命周期意味着即便 SVID 泄漏,攻击者能利用的时间窗口也极短。这是 SPIFFE 和传统长期证书最大的不同。
1.3 SPIRE 架构#
┌────────────────────────────────────────────────────────────────┐
│ SPIRE Server │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Node Attestor│ │ Registration │ │ Signing CA │ │
│ │ (k8s, aws, │ │ Entry Store │ │ (self/ upstream) │ │
│ │ join-token)│ │ (DB) │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└───────────────▲────────────────────────────────▲───────────────┘
│ node attestation │ SVID issuance
┌────────┴───────────┐ ┌───────┴────────┐
│ SPIRE Agent (k8s) │ │ SPIRE Agent(VM)│
│ │ │ │
│ Workload API │ │ Workload API │
│ (unix socket) │ │ (unix socket) │
└──────┬─────────────┘ └───────┬────────┘
│ attest + fetch SVID │
┌──────▼───────┐ ┌─────▼────────┐
│ Pod A (app1) │ │ nginx on VM │
│ Pod B (app2) │ │ postgres │
└──────────────┘ └──────────────┘
SPIRE Server 是全局单点(一般 3~5 副本 HA),负责:
- 管理注册表(哪个选择器对应哪个 SPIFFE ID)
- 签发 SVID
- 管理信任域的签名 CA(也可以桥接外部 CA,比如 AWS PCA、Vault PKI)
SPIRE Agent 部署在每个节点(Kubernetes 里是 DaemonSet,VM 上是 systemd),负责:
- 通过节点证明(node attestation)向 Server 证明自己在哪台机器上
- 通过工作负载证明(workload attestation)识别本机的工作负载
- 暴露 Workload API(一个 Unix socket)给应用调用,返回 SVID
1.4 双重证明:节点证明 + 工作负载证明#
这是 SPIRE 最巧妙的设计。传统方案里,让一个应用"证明自己是谁"是个鸡生蛋的问题——你总得先有一把密钥,密钥从哪来?SPIRE 的答案是:先证明机器,再在机器内部通过进程选择器证明应用。
- 节点证明:Agent 启动时,使用"机器身份"向 Server 认证。机器身份可以是 EC2 instance identity document、EKS ServiceAccount token、云厂商元数据、或者预共享的 join token。
- 工作负载证明:Agent 拿到 SVID 后,Pod/进程通过 Unix socket 请求 SVID。Agent 查看调用者的进程信息(PID、UID、K8s labels、namespace、container image hash…),匹配到对应的注册表条目,然后才发 SVID。
关键:应用本身不需要持有任何长期凭据。连接 Unix socket 就能拿到当前"该我拥有"的 SVID。这是为什么 SPIFFE 能做到"零密钥分发"。
二、生产部署:SPIRE on Kubernetes#
2.1 选择部署方式:Helm、Operator 还是手写 manifest#
2025 年的生产环境我强烈推荐使用 spire-controller-manager + spire-crds 的模式,通过 ClusterSPIFFEID 和 ClusterFederatedTrustDomain 这两个 CRD 声明式管理,不再手动调 SPIRE Server API 注册工作负载。官方 Helm chart spiffe/spire 已经把这一套封装好了。
老的"纯 Helm + manual registration API 调用"模式维护成本高,不推荐新项目采用。
2.2 Helm values 示例#
global:
spire:
clusterName: us-prod
trustDomain: prod.example.com
jwtIssuer: https://spire.prod.example.com
recommendations:
create: true
spire-server:
replicaCount: 3
ca_subject:
country: US
organization: Example Corp
common_name: SPIRE Server CA (prod)
ca_ttl: 87600h # CA 10 年
default_x509_svid_ttl: 1h
default_jwt_svid_ttl: 5m
dataStore:
sql:
databaseType: postgres
host: spire-db.prod.internal
port: 5432
databaseName: spire
username: spire
# 密码走 External Secrets 注入
passwordSecretRef:
name: spire-db-password
key: password
nodeAttestor:
k8sPsat:
enabled: true
serviceAccountAllowList: ["spire:spire-agent"]
keyManager:
awsKms:
enabled: true
region: us-west-2
keyMetadata:
kmsKeyPolicy: "arn:aws:kms:..."
controllerManager:
enabled: true
identities:
clusterSPIFFEIDs:
default:
enabled: false # 我们不用 "catch-all",强制显式声明
spire-agent:
sockets:
hostBasePath: /run/spire
nodeAttestor:
k8sPsat:
enabled: true
workloadAttestors:
k8s:
enabled: true
useNewContainerLocator: true # 1.10+ 新的容器定位器,支持 containerd 2.0
disableContainerSelectors: false
几个关键选择的理由:
- PostgreSQL 作为 datastore:SQLite 只能单副本,生产必须用外部 SQL。MySQL/PostgreSQL 都行,我们选 PostgreSQL 因为 RDS 管理方便。datastore 每个 trust domain 一个,不要多集群共享。
- AWS KMS 作为 KeyManager:SPIRE 的 CA 私钥如果存本地磁盘,HA 部署时需要同步,麻烦且不安全。用 KMS 把私钥托管起来,三副本共享同一把 KMS key,Server 崩溃重建后无感恢复。阿里云环境可以用 KMS,自建可以用 HashiCorp Vault Transit。
k8s_psat(Projected Service Account Token)节点证明:比老的k8s_sat安全,因为 token 有 audience、有过期时间、绑定 Pod。defaultidentity 关掉:默认 chart 会给所有 Pod 一个 catch-all 身份,这会让你失去"谁没身份"的可见性。我坚持强制显式声明。
2.3 给 Pod 发身份:ClusterSPIFFEID#
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: payments-checkout
spec:
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
podSelector:
matchLabels:
app: checkout-service
namespaceSelector:
matchLabels:
spiffe.io/managed: "true"
dnsNameTemplates:
- "checkout.payments.svc.cluster.local"
- "checkout.payments.internal"
ttl: 30m
workloadSelectorTemplates:
- "k8s:ns:{{ .PodMeta.Namespace }}"
- "k8s:sa:{{ .PodSpec.ServiceAccountName }}"
- "k8s:pod-label:app:{{ index .PodMeta.Labels \"app\" }}"
注意几个点:
spiffeIDTemplate用ns/<namespace>/sa/<sa>结构,和 K8s 原生的 ServiceAccount 对齐,IAM 做映射时非常方便。dnsNameTemplates会写进 X.509 的 SAN DNS 字段,客户端校验证书时可以按 DNS 名验证(便于和传统 PKI 客户端兼容)。ttl: 30m是个折中。设太短(5m)会给 SPIRE Server 和 CA 带来压力,设太长则"撤销"变得无意义。30m 对于大多数业务够用。workloadSelectorTemplates是工作负载证明的选择器,必须同时匹配才会下发 SVID。加pod-label是为了让同名 SA 下不同 app 的 Pod 拿到不同身份。
2.4 应用怎么用 SVID#
最简单的用法是通过 SPIFFE Workload API 的 SDK。Go 版本:
import (
"context"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
func main() {
ctx := context.Background()
source, err := workloadapi.NewX509Source(ctx,
workloadapi.WithClientOptions(
workloadapi.WithAddr("unix:///run/spire/sockets/agent.sock"),
),
)
if err != nil { log.Fatal(err) }
defer source.Close()
tlsConfig := tlsconfig.MTLSClientConfig(
source, source,
tlsconfig.AuthorizeMemberOf(spiffeid.RequireTrustDomainFromString("prod.example.com")),
)
client := &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
}
resp, err := client.Get("https://checkout.payments.internal:8443/api/v1/orders")
...
}
NewX509Source 会在后台自动续期,应用永远用的是新鲜 SVID,不需要关心证书到期。服务端类似,用 tlsconfig.MTLSServerConfig,并通过 AuthorizeAny() 或者 AuthorizeID(spiffeid.Must(...)) 限定能调自己的身份列表。
对于无法改代码的遗留应用,有三种选择:
- spiffe-helper:一个 sidecar,它把 SVID 和信任包(trust bundle)写成文件,定时 rotate,应用像读传统证书一样读文件即可。
- Istio + SPIRE 集成:Istio 1.14+ 支持用 SPIRE 作为 CA,Envoy 直接从 SPIRE 取 SVID,应用完全无感。
- SPIFFE-CSI driver:把 Agent socket 挂到 Pod 里,不需要每个 Pod 都走 hostPath。
我们生产里三种都在用,Istio 场景最多,次之是 spiffe-helper。
2.5 Istio 集成#
Istio 1.14 之后支持 pilot-agent 从 SPIRE Workload API 取证书。核心配置:
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
trustDomain: prod.example.com
defaultConfig:
proxyMetadata:
ISTIO_META_CERT_SIGNER: spire
values:
global:
caAddress: "unix:///run/spire/sockets/agent.sock"
pilot:
env:
ENABLE_CA_SERVER: "false"
PILOT_CERT_PROVIDER: spiffe
Istio sidecar 启动时会挂载 SPIRE Agent 的 socket,从中取 X.509-SVID 作为 Envoy 的工作负载证书。这样 Istio 的 mTLS 就完全基于 SPIFFE 身份,而不是 Istio 内建的 Citadel。好处是:
- 统一身份:VM 上的传统服务也用 SPIFFE 身份,和 K8s Pod 互信
- 可验证:Envoy 的 metric 里能看到对端 SPIFFE ID,审计方便
- CA 托管:用 KMS 管 CA 私钥,比 Citadel 默认本地存安全
三、混合场景:把虚拟机纳入 SPIFFE 信任域#
很多公司 K8s 之外还跑着大量 VM(数据库、老业务、Windows 工作负载),让这些 VM 也进入 SPIFFE 信任域是零信任落地的关键一步。
3.1 VM 上部署 Agent#
# Ubuntu 22.04 示例
curl -L https://github.com/spiffe/spire/releases/download/v1.10.2/spire-1.10.2-linux-amd64-musl.tar.gz | \
sudo tar -xz -C /opt
sudo useradd --system --home /var/lib/spire spire
sudo install -d -o spire -g spire /var/lib/spire /run/spire /etc/spire
sudo tee /etc/spire/agent.conf <<'EOF'
agent {
data_dir = "/var/lib/spire"
log_level = "INFO"
server_address = "spire.prod.example.com"
server_port = "8081"
socket_path = "/run/spire/agent.sock"
trust_domain = "prod.example.com"
trust_bundle_path = "/etc/spire/bootstrap.crt"
}
plugins {
NodeAttestor "aws_iid" {
plugin_data { }
}
KeyManager "disk" {
plugin_data {
directory = "/var/lib/spire"
}
}
WorkloadAttestor "unix" {
plugin_data {
discover_workload_path = true
}
}
WorkloadAttestor "systemd" {
plugin_data { }
}
}
EOF
sudo systemctl enable --now spire-agent
几个关键点:
aws_iid节点证明:利用 EC2 instance identity document,每台机器都有唯一的 IID,SPIRE Server 可以绑定到 instance ID、region、账号,拒绝不符合的。systemd工作负载证明:可以根据 systemd unit name 发 SVID,非常适合 VM 上的传统服务。bootstrap.crt:Agent 首次连接 Server 需要信任 Server 的 CA,这个是通过离线分发的 bootstrap 证书建立的。生产里通过 cloud-init 或者 Ansible 推下去。
3.2 为 systemd 服务发身份#
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: db-proxy-vm
spec:
spiffeIDTemplate: "spiffe://prod.example.com/vm/db-proxy/{{ .NodeMeta.Hostname }}"
nodeSelector:
matchLabels:
node.type: "vm"
node.role: "db-proxy"
workloadSelectorTemplates:
- "systemd:unit:db-proxy.service"
- "unix:uid:999"
注意 VM 场景用 ClusterSPIFFEID 的方式要通过 spire-controller-manager 的 VM 适配模式(1.9+ 支持)。老版本需要手动 spire-server entry create 命令行注册。
3.3 spiffe-helper 给非感知应用签证书#
# /etc/spire/helper.conf
agent_address = "/run/spire/agent.sock"
cmd = "/bin/systemctl"
cmd_args = "reload nginx"
cert_dir = "/etc/nginx/spiffe"
svid_file_name = "svid.crt"
svid_key_file_name = "svid.key"
svid_bundle_file_name = "bundle.crt"
renew_signal = "SIGHUP"
运行 spiffe-helper 进程,它会每 30 秒检查 SVID 是否快过期,过期前重新从 Workload API 取新的,写到 cert_dir,然后发 SIGHUP 给应用。nginx/postgres 都能用这种方式平滑换证。
四、信任域联邦:跨集群、跨云互信#
生产环境很少只有一个信任域,比如:
- 不同集群(us-prod / cn-prod)一个信任域一个
- 不同环境(prod / staging)必须隔离
跨信任域通信需要联邦:两个信任域互相交换 trust bundle,让对方的 CA 被己方信任。
SPIRE 从 1.5 开始支持声明式联邦,1.10 里已经非常稳定。配置方式:
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterFederatedTrustDomain
metadata:
name: cn-prod
spec:
trustDomain: cn.prod.example.com
bundleEndpointURL: https://spire.cn.prod.example.com/bundle
bundleEndpointProfile:
type: https_spiffe
endpointSPIFFEID: spiffe://cn.prod.example.com/spire/server
trustDomainBundle: |-
-----BEGIN CERTIFICATE-----
MIID....(bootstrap bundle)
-----END CERTIFICATE-----
联邦后,us-prod 的 Pod 可以和 cn.prod.example.com 下的工作负载做 mTLS,AuthorizeID 里指定对方的 SPIFFE ID 即可:
tlsConfig := tlsconfig.MTLSClientConfig(
source, source,
tlsconfig.AuthorizeID(spiffeid.RequireFromString(
"spiffe://cn.prod.example.com/ns/data/sa/sync-service",
)),
)
关键权限模型:联邦只是"互相认识对方的 CA",不等于"互相授权"。授权依然需要上层策略(比如 OPA)决定哪个 SPIFFE ID 能调哪个 API。我们的实践是:
- 联邦在 SPIRE 层建立
- 调用授权在 Istio AuthorizationPolicy 或者 OPA 层决定
- 业务层再做细粒度授权(tenant、user)
五、运营实战:真实踩坑与经验#
5.1 datastore 必须定期备份#
SPIRE Server 的 datastore 存了所有 entry 和 CA 信息。datastore 一丢,整个信任域就没了,所有 Agent 需要重新 bootstrap,所有应用需要重连,是一次全站事故。
我们的备份策略:
- PostgreSQL RDS 每日快照 + 点时间恢复
- 每周导出一次 entry 列表为 JSON 到 S3:
spire-server entry show -output json > entries-$(date +%F).json aws s3 cp entries-$(date +%F).json s3://spire-backup/ - CA 配置文件 + KMS key ARN 放在 Git,用 sealed-secrets 加密
5.2 Agent 崩溃怎么办?#
Agent 崩溃是最容易被忽略的故障,因为它对控制面无感(SPIRE Server 不会 crash),但对数据面是灾难:Agent 所在节点的所有 Pod 无法获取新的 SVID,30 分钟后 SVID 过期,所有 mTLS 连接报错。
防御措施:
- Agent DaemonSet 配 liveness probe:探测
/run/spire/agent.sock是否响应,不响应就重启 - Prometheus 监控
spire_agent_svids_issued_total增长率,若某节点 10 分钟无增长告警 - 应用侧做重试和降级:go-spiffe SDK 在连不上 Agent 时会返回错误,应用要能处理(至少重试几次,不能让一个 Agent 问题雪崩到整个业务)
真实案例:2025 年 3 月某次 kubelet 滚动重启时 Agent 进入 CrashLoopBackOff(因为 hostPath socket 残留了坏 symlink),整个节点的 Pod 连续 15 分钟无法续签,直到我们手动删 symlink。事后我们给 Agent 加了 initContainer 清理残留 socket:
initContainers:
- name: cleanup-socket
image: busybox:1.36
command: ["sh", "-c", "rm -f /run/spire/sockets/agent.sock"]
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
5.3 SPIRE Server HA 的 split-brain 风险#
SPIRE Server 三副本共享同一个 datastore,但 CA 签名状态需要协调。1.8 之前偶发 split-brain(两个 Server 同时认为自己是 CA leader),1.9 引入了基于 datastore 的 lease,1.10 更稳了。但即便如此,我建议:
- 不要跨 Region 部署一个 SPIRE Server(延迟对 lease 不友好)
- 每个 Region 一个独立的信任域(region.prod.example.com),通过联邦互信
- Server 副本数 3,不要 5 或 7(datastore lease 的协调成本平方级增长)
5.4 注册表膨胀#
spire-controller-manager 会为每个匹配的 Pod 创建 registration entry。一个 5000 Pod 集群 entry 数量就是 5000+,大量短生命周期 Pod(CronJob、CI runner)会导致 entry 频繁增删,datastore 压力大。
优化:
- CronJob/Job 类工作负载用父选择器+通配的方式注册,不要给每个 Pod 单独 entry
- 把 entry 的
admin字段关掉(减少访问控制开销) spire-controller-manager的gcInterval可以调到 5 分钟一次(默认 10 秒太频繁)
5.5 可观测性#
SPIRE 本身暴露 Prometheus metrics,关键指标:
# Server 端
spire_server_registration_entries{} gauge # entry 总数
spire_server_svid_x509_signed_total # 签发速率
spire_server_datastore_sql_errors_total # datastore 错误
spire_server_node_attestation_success_total # 节点证明成功数
# Agent 端
spire_agent_svids_updated_total # SVID 更新次数
spire_agent_workload_api_fetch_x509_svid_total # 工作负载请求数
spire_agent_manager_cache_size # 本地缓存大小
告警规则示例:
- alert: SpireAgentDown
expr: up{job="spire-agent"} == 0
for: 2m
labels: { severity: critical }
- alert: SpireSignRateAbnormal
expr: |
rate(spire_server_svid_x509_signed_total[5m])
/ rate(spire_server_svid_x509_signed_total[1h] offset 1h) > 3
for: 10m
annotations:
summary: "SPIRE 签发速率异常升高,可能有 Agent 风暴"
六、和 Vault/External Secrets 的对比#
经常有人问:“我都有 Vault 了,还需要 SPIRE 吗?” 简短回答:需要,它们解决的是不同层次的问题。
| 维度 | Vault / External Secrets | SPIRE |
|---|---|---|
| 解决的问题 | 密钥/配置分发 | 工作负载身份 |
| 凭据类型 | 长期凭据(DB 密码、API key) | 短期 SVID |
| 身份来源 | 需要 bootstrap secret 或 K8s SA | 基于机器证明+进程证明 |
| 适用场景 | 应用需要的第三方服务凭据 | 服务间 mTLS、零信任 |
| 可不可以互补 | 可以且应该 | 可以且应该 |
典型组合:Vault Agent 用 SPIRE SVID 作为认证方式向 Vault 取 DB 密码。这样 Vault 的 auth/spiffe 后端验证 SVID,就不需要预先分发 token。
七、和传统 PKI 的兼容#
不是所有服务都能改成调 Workload API。很多 Java 老服务只认 JKS 文件,怎么办?
- spiffe-helper 输出 PKCS12(1.9+ 支持),Java 能直接用
- cert-manager 的 SPIFFE 集成:cert-manager 1.15+ 支持把 SPIRE 作为 issuer,自动生成 Certificate 资源
- 把 SPIRE 作为一个证书转换器:SPIRE 签发 SVID,应用侧挂 spiffe-helper 转换成传统 PEM/JKS/P12
我们的一个老 Spring Boot 服务就是走第三条路,spiffe-helper 写出 keystore.p12 和 truststore.p12,Spring 的 SSL 配置指向这两个文件,每 10 分钟 rotate 一次。业务零改动。
八、落地路线图建议#
最后给一个循序渐进的落地建议,供还没开始的团队参考:
第 1 阶段(1~2 个月):只部署 SPIRE Server + Agent,发 SVID 但不强制使用。让开发团队熟悉 SPIFFE ID 的命名约定。
第 2 阶段(2~4 个月):选一个简单业务做 pilot,跑通 Go/Java SDK 集成,验证 SVID 续期、故障回滚。同时搞定 Istio 集成,让大部分 mTLS 流量切到 SPIFFE 证书。
第 3 阶段(4~6 个月):推广到所有 K8s 业务,强制新服务用 SPIFFE 身份。开始接入非 K8s 工作负载(VM、数据库代理、CI runner)。
第 4 阶段(6~12 个月):打通联邦,多集群互信;Vault 对接 SPIFFE 认证;老 PKI 替换下线。
SPIFFE/SPIRE 不是"一键搞定"的工具,是一整套身份体系的建设。我们跑了两年多才真正让它从"写 PPT 的 slogan"变成每天能用的生产能力。配合 Falco + Cilium L7 + Kyverno,身份、调用、策略这三层都能落到可审计的工程实践上,这是我愿意继续投入的方向。






