spire-agent 详解(含全部 agent 插件)
分析对象:SPIRE v1.15.2,聚焦
pkg/agent/与pkg/agent/plugin/。 所有路径:行号相对 SPIRE 仓库根,均经一手代码核对。 配套文档:整体架构 · SVID 颁发全流程 · Server 详解 · K8S Agent 详解
一句话定位:spire-agent 是运行在每个工作负载节点上的本地守护进程——先用 NodeAttestor 向 server 证明"本节点是谁"换取自身 SVID,再用 KeyManager 托管私钥、周期性预签并缓存工作负载 SVID,最后用 WorkloadAttestor 对每个调用进程现算 selectors、通过 SPIFFE Workload API 下发对应身份;需要"离线注入"的场景则用 SVIDStore 把 SVID 写入外部密钥库。这四类插件就是 agent 的全部可插拔能力。
Agent 职责与引导
Agent 的装配中枢是 Agent.Run(pkg/agent/agent.go:63)。相比 server 的纯依赖注入,它多了一段**"入网引导循环"**:节点必须先完成认证拿到 agent SVID,后续所有工作才有资格进行。启动骨架(agent.go:63-324)简化如下:
diskutil.CreateDataDirectory → storage.Open(本地持久化,agent.go:72)
→ telemetry.NewMetrics → catalog.Load(加载 4 类插件,agent.go:132)
→ 选 NodeAttestor:join token 用内置 wrapper,否则用 catalog 插件(agent.go:149-152)
→ 【引导循环 agent.go:190-261】loadBundle → a.attest(节点认证) → 成功 break / 失败 backoff 重试
→ newManager(SVID 缓存与轮换,agent.go:265)
→ newSVIDStoreService(agent.go:270) + workload_attestor.New(agent.go:271)
→ newEndpoints(Workload API,agent.go:277)
→ TaskRunner 并发:metrics / mgr.Run / storeService.Run / agentEndpoints.ListenAndServe / catalog 重配(agent.go:283-289)NodeAttestor 的选取有一个特例——如果通过 CLI/HCL 提供了 join token,则不走 catalog,而是用内置的 nodeattestor.JoinToken 包装器:
// pkg/agent/agent.go:149
nodeAttestor := nodeattestor.JoinToken(a.c.Log, a.c.JoinToken)
if a.c.JoinToken == "" {
nodeAttestor = cat.GetNodeAttestor() // 否则取 catalog 中唯一的 NodeAttestor 插件
}引导循环是 agent 冷启动的心脏(agent.go:190-261):先从本地 storage 取 bootstrap bundle(取不到再从配置的 TrustBundleSources 拉),然后调用 a.attest 完成节点认证;成功即 break,失败按退避策略重试(普通模式最长 1 分钟,rebootstrap 模式最长 24 小时):
// pkg/agent/agent.go:190
for {
bootstrapTrustBundle, err := sto.LoadBundle()
if errors.Is(err, storage.ErrNotCached) {
bootstrapTrustBundle, insecureBootstrap, err = a.c.TrustBundleSources.GetBundle()
}
if err == nil {
as, err = a.attest(ctx, sto, cat, metrics, nodeAttestor, bootstrapTrustBundle, insecureBootstrap)
if err == nil {
...
break // 认证成功,携带 AttestationResult 退出循环
}
if status.Code(err) == codes.PermissionDenied { return err } // 被 server 拒绝,直接失败
}
nextDuration := attBackoff.NextBackOff()
if nextDuration == backoff.Stop { return err }
select {
case <-ctx.Done(): return ctx.Err()
case <-attBackoffClock.After(nextDuration): continue
}
}a.attest(agent.go:377)只是把配置组装给节点认证子系统 attestor/node,真正的双向流认证过程见 SVID 颁发全流程 · 链路一。引导成功后,AttestationResult(含 agent SVID、私钥、trust bundle、Reattestable 标志)交给 newManager,由 manager 内部的 svid.Rotator 接管 agent SVID 的后续轮换。
Agent 的整体装配与在架构中的位置见 整体架构 · §3 Agent 侧内部模块;本文只聚焦插件。
插件框架
Agent 端的插件目录在 pkg/agent/catalog/catalog.go。它对外暴露一个类型化的 Catalog 接口,恰好对应四类 agent 插件:
// pkg/agent/catalog/catalog.go:31
type Catalog interface {
GetKeyManager() keymanager.KeyManager // 恰好 1 个
GetNodeAttestor() nodeattestor.NodeAttestor // 恰好 1 个
GetSVIDStoreNamed(name string) (svidstore.SVIDStore, bool) // 0..N 个,按名取
GetWorkloadAttestors() []workloadattestor.WorkloadAttestor // 至少 1 个,全部
}注意接口形态已经暗示了各类插件的数量约束与调用方式:NodeAttestor / KeyManager 返回单个;WorkloadAttestor 返回整个切片(要并行全调);SVIDStore 按名字查(因为一条 store 类型的注册条目要精确路由到某一个 store 插件)。
内置与外部插件完全同构,都走 gRPC——这是 SPIRE catalog 的核心设计,agent 端也不例外。catalog.Load(catalog.go:85)把四个插件仓库(keyManagerRepository 等)交给通用框架 pkg/common/catalog 加载:内置插件在进程内起 gRPC server、经内存管道回环连接;外部插件走 hashicorp/go-plugin 子进程。上层拿到的都是同一套 gRPC 客户端 facade,对二者无感知(机制详见 整体架构 · §4)。
每类插件在 agent catalog 里都有一个注册文件,声明三件事:数量约束(Constraints)、gRPC facade 版本(Versions)、内置实现清单(BuiltIns)。以 NodeAttestor 为例:
// pkg/agent/catalog/nodeattestor.go:26
func (repo *nodeAttestorRepository) Constraints() catalog.Constraints { return catalog.ExactlyOne() }
func (repo *nodeAttestorRepository) Versions() []catalog.Version { return []catalog.Version{nodeAttestorV1{}} }
func (repo *nodeAttestorRepository) BuiltIns() []catalog.BuiltIn {
return []catalog.BuiltIn{ awsiid.BuiltIn(), azuremsi.BuiltIn(), /* ... */ x509pop.BuiltIn() }
}四类约束的语义(pkg/common/catalog/constraints.go:7,Constraints{Min,Max},Max==0 表示无上限):
| 约束助手 | {Min,Max} | 含义 | 用于 |
|---|---|---|---|
ExactlyOne() | {1,1} | 必须恰好 1 个 | NodeAttestor、KeyManager |
AtLeastOne() | {1,0} | 至少 1 个,无上限 | WorkloadAttestor |
ZeroOrMore() | {0,0} | 任意个(含 0) | SVIDStore |
MaybeOne() | {0,1} | 至多 1 个 | (agent 未用;server 的 UpstreamAuthority) |
四类插件一览(约束与内置实现均以 pkg/agent/catalog/*.go 的 Constraints()/BuiltIns() 实测为准):
| 插件类型 | Go 接口 | 约束 | 内置实现(数量) | 调用时机 |
|---|---|---|---|---|
| NodeAttestor | nodeattestor.NodeAttestor | ExactlyOne | join_token, aws_iid, gcp_iit, azure_msi, azure_imds, k8s_psat, x509pop, sshpop, tpm_devid, http_challenge(10) | agent 入网(启动 + 重认证) |
| KeyManager | keymanager.KeyManager | ExactlyOne | disk, memory(2) | 生成/托管所有私钥 |
| WorkloadAttestor | workloadattestor.WorkloadAttestor | AtLeastOne | unix, docker, k8s, systemd, windows(5) | 每次 Workload API 调用 |
| SVIDStore | svidstore.SVIDStore | ZeroOrMore | aws_secretsmanager, gcp_secretmanager(2) | 带 -storeSVID 的条目 |
一个额外约束:内置 join_token 不允许被外部插件覆盖(
catalog.go:86):if c, ok := ...Find(nodeAttestorType, jointoken.PluginName); ok && c.IsEnabled() && c.IsExternal()则直接报错。
下面逐类详解。
NodeAttestor 插件
做什么
Agent 入网时,由 NodeAttestor 在 agent 侧生成 attestation 数据向 server 证明"本节点的身份",从而换取一张代表 agent 自己的 SVID。它与 server 侧同名的 NodeAttestor 插件成对工作:agent 侧负责产出证据(读云平台 IID、读 TPM、出示证书……)并应答挑战;server 侧负责验证证据、派生 SPIFFE ID、决定节点 selectors。约束是 ExactlyOne——一个 agent 只能配一种入网方式。
Go 接口
// pkg/agent/plugin/nodeattestor/nodeattestor.go:10
type NodeAttestor interface {
catalog.PluginInfo
// 用 server 流完成认证;ServerStream 产生的错误原样返回
Attest(ctx context.Context, serverStream ServerStream) error
}
// ServerStream 是插件向 server 发送 attestation 数据 / 挑战应答的通道
type ServerStream interface { // :21
SendAttestationData(ctx context.Context, attestationData AttestationData) ([]byte, error)
SendChallengeResponse(ctx context.Context, response []byte) ([]byte, error)
}
type AttestationData struct { Type string; Payload []byte } // :28内置实现
| 插件名 | 证据来源 | 交互形态 | 说明 |
|---|---|---|---|
| join_token | 一次性预共享令牌 | 无挑战(payload 即 token) | 特例,由 agent 特殊代码路径注入,见下 |
| aws_iid | EC2 实例身份文档 + 签名(RSA2048) | 单发 payload | iid.go:74 读 IMDS,iid.go:91 发 payload |
| gcp_iit | GCP 实例身份 token(JWT) | 单发 payload | iit.go:79,audience spire-gcp-node-attestor(iit.go:25) |
| azure_msi | Azure MSI token | 单发 payload | 托管身份令牌 |
| azure_imds | Azure IMDS 元数据 | 单发 payload | 实例元数据服务 |
| k8s_psat | 投影的 ServiceAccount Token | 单发 payload | psat.go:87;详见 K8S Agent 详解 |
| x509pop | X.509 证书 + 私钥 | 挑战应答(拥有性证明) | x509pop.go:81 |
| sshpop | SSH 证书 + 私钥 | 挑战应答 | sshpop.go:39,RespondToChallenge(:64) |
| tpm_devid | TPM DevID 证书 + TPM 密钥 | 挑战应答 | 硬件根信任 |
| http_challenge | agent 暴露的 HTTP 端点 | 挑战应答(server 回拉 nonce) | httpchallenge.go:110,hostname/advertised_port(:46/:49) |
出处:pkg/agent/catalog/nodeattestor.go:36-49 的 BuiltIns()。
配置要点
ExactlyOne——agent.conf 的 plugins {} 里必须且只能配一个 NodeAttestor,且必须与 server 端配置了同名插件。若用 join token,则在 agent 命令行 -joinToken 或配置里给出 token 值(此时无需在 plugins 中显式列 join_token,但通常会声明以便文档化)。云平台类(aws_iid/gcp_iit/azure_*)几乎零配置(直接读元数据服务);证书类(x509pop/sshpop/tpm_devid)需配私钥与证书路径。
关键代码走读:三层适配 + 挑战应答流
NodeAttestor 有三层结构,理解它就理解了"内置插件也走 gRPC":
① 插件实现层——以 x509pop 为例,插件实现 gRPC 的 AidAttestation 流:先发证书 payload,收到挑战后用私钥算响应(拥有性证明 PoP):
// pkg/agent/plugin/nodeattestor/x509pop/x509pop.go:81
func (p *Plugin) AidAttestation(stream nodeattestorv1.NodeAttestor_AidAttestationServer) error {
data, _ := p.loadConfigData(stream.Context())
stream.Send(&...PayloadOrChallengeResponse{Data: &..._Payload{Payload: data.attestationPayload}}) // 发证书
resp, _ := stream.Recv() // 收挑战
challenge := new(x509pop.Challenge); json.Unmarshal(resp.Challenge, challenge)
response, _ := x509pop.CalculateResponse(data.privateKey, challenge) // 用私钥算响应
responseBytes, _ := json.Marshal(response)
return stream.Send(&...PayloadOrChallengeResponse{Data: &..._ChallengeResponse{ChallengeResponse: responseBytes}})
}② V1 facade 层(nodeattestor/v1.go:18)——把插件的 gRPC 流"翻译"成上层的 Attest(ctx, ServerStream),在插件流与server 流之间来回搬运 payload/challenge:
// pkg/agent/plugin/nodeattestor/v1.go:18
func (v1 *V1) Attest(ctx context.Context, serverStream ServerStream) error {
pluginStream, _ := v1.NodeAttestorPluginClient.AidAttestation(ctx) // 打开插件流
payloadOrChallengeResponse, _ := pluginStream.Recv() // 从插件取 payload
payload := payloadOrChallengeResponse.GetPayload()
challenge, _ := serverStream.SendAttestationData(ctx, AttestationData{Type: v1.Name(), Payload: payload}) // 发给 server
for {
if challenge == nil { return nil } // server 直接给了 SVID,结束
pluginStream.Send(&nodeattestorv1.Challenge{Challenge: challenge}) // 把挑战转给插件
pcr, _ := pluginStream.Recv() // 取插件的挑战响应
challenge, _ = serverStream.SendChallengeResponse(ctx, pcr.GetChallengeResponse()) // 回给 server
}
}③ ServerStream 层(attestor/node/node.go:301)——SendAttestationData 把 payload 连同不含 URI SAN 的 CSR 打包成 AttestAgentRequest,首次调用时惰性打开到 server 的 gRPC 双向流 AttestAgent;Recv 到的要么是 Challenge(继续循环),要么是最终 Result{Svid, Reattestable}:
// pkg/agent/attestor/node/node.go:310
func (ss *ServerStream) SendAttestationData(ctx, attestationData) ([]byte, error) {
return ss.sendRequest(ctx, &agentv1.AttestAgentRequest{Step: &..._Params_{Params: &..._Params{
Data: &types.AttestationData{Type: attestationData.Type, Payload: attestationData.Payload},
Params: &agentv1.AgentX509SVIDParams{Csr: ss.Csr}, // CSR 不含 SPIFFE ID
}}})
}
// sendRequest(:334):ss.stream 为空时 ss.Client.AttestAgent(ctx) 打开双向流;Recv 到 Challenge 则返回挑战,否则解析出 SVIDCSR 的生成在 newSVID(node.go:229)——util.MakeCSRWithoutURISAN(key),SPIFFE ID 由 server 依据认证结果决定,主体不能自证身份。
join_token 的特例:它的插件实现 AidAttestation 只返回错误(jointoken/join_token.go:30:"join token was not provided"),因为真正的逻辑走 agent 的特殊代码路径——nodeattestor.JoinToken 包装器(jointoken.go:11)直接把 token 作为 payload 发出,且不接受任何挑战:
// pkg/agent/plugin/nodeattestor/jointoken.go:23
func (plugin joinToken) Attest(ctx context.Context, serverStream ServerStream) error {
challenge, err := serverStream.SendAttestationData(ctx, AttestationData{Type: plugin.Name(), Payload: []byte(plugin.token)})
switch {
case err != nil: return err
case challenge != nil: return plugin.Error(codes.Internal, "server issued unexpected challenge") // join_token 不该被挑战
default: return nil
}
}k8s_psat 的 agent 侧只读取投影 token 并作为 payload 单发(
psat.go:87的AidAttestation→loadTokenFromFile→json.Marshal(k8s.PSATAttestationData{Cluster, Token})),无挑战。它与 server 侧k8s_psat的配对、以及在 K8s 里如何部署,详见 K8S Agent 详解。
KeyManager 插件
做什么
KeyManager 生成并托管 agent 用到的所有私钥——既包括 agent 自身 SVID 的私钥,也包括为每个工作负载 SVID 生成的私钥。核心安全约束:私钥永不出机,插件只对外提供"签名器"(crypto.Signer)句柄,调用方拿不到私钥字节。约束是 ExactlyOne。
Go 接口
// pkg/agent/plugin/keymanager/keymanager.go:16
type KeyManager interface {
catalog.PluginInfo
GenerateKey(ctx context.Context, id string, keyType KeyType) (Key, error) // 存在则覆盖
GetKey(ctx context.Context, id string) (Key, error)
GetKeys(ctx context.Context) ([]Key, error)
}
type Key interface { crypto.Signer; ID() string } // :32,只暴露签名器
// KeyType:ECP256 / ECP384 / RSA2048 / RSA4096(:40)内置实现
| 插件名 | 私钥持久化 | 适用 | 关键位置 |
|---|---|---|---|
| disk | 落盘到 <directory>/keys.json(PKCS#8,原子写,0600) | 重启后能恢复 agent SVID / 工作负载密钥,生产默认 | disk/disk.go:33 |
| memory | 仅内存,进程退出即失 | 测试 / 无状态短生命周期节点 | memory/memory.go:19 |
两者共享同一套内存实现 keymanagerbase.Base(base/keymanagerbase.go),disk 只是额外挂了一个 WriteEntries 回调把条目刷盘。出处:pkg/agent/catalog/keymanager.go:27-32。
配置要点
- disk:
directory必填(disk.go:71,为空报错;启动时会建目录并做写探针.probe)。密钥写入keys.json。 - memory:无配置。
- 全局:
agent.conf的workload_x509_svid_key_type决定工作负载私钥类型;agent 自身 SVID 固定用 ECP256(见下)。
关键代码走读:base 生成 + SVID 双槽
base 的 GenerateKey(base/keymanagerbase.go:124)生成密钥条目、写入内存 map,若配了 WriteEntries 回调(disk 才有)则落盘,失败会回滚内存条目:
// pkg/agent/plugin/keymanager/base/keymanagerbase.go:124
func (m *Base) generateKey(ctx, req) (*keymanagerv1.GenerateKeyResponse, error) {
newEntry, _ := m.generateKeyEntry(req.KeyId, req.KeyType)
m.mu.Lock(); defer m.mu.Unlock()
oldEntry, hasEntry := m.entries[req.KeyId]
m.entries[req.KeyId] = newEntry
if m.config.WriteEntries != nil { // disk:刷盘
if err := m.config.WriteEntries(ctx, entriesSliceFromMap(m.entries), newEntry); err != nil {
if hasEntry { m.entries[req.KeyId] = oldEntry } else { delete(m.entries, req.KeyId) } // 回滚
return nil, err
}
}
return &keymanagerv1.GenerateKeyResponse{PublicKey: clonePublicKey(newEntry.PublicKey)}, nil
}disk 的落盘用 PKCS#8 序列化后原子写(disk/disk.go:171 writeEntries → diskutil.AtomicWritePrivateFile,路径 keys.json,disk.go:195)。
agent SVID 的"双槽"轮换:agent 自身 SVID 的密钥由专门的 SVIDKeyManager(svidkeymanager.go)包装,它在 agent-svid-A / agent-svid-B 两个 slot 间交替,以支持"生成新密钥的同时旧密钥仍可用"的平滑轮换,固定 ECP256:
// pkg/agent/plugin/keymanager/svidkeymanager.go:28
func (s svidKeyManager) GenerateKey(ctx context.Context, currentKey Key) (Key, error) {
keyID := "agent-svid-A"
if currentKey != nil && currentKey.ID() == keyID { keyID = "agent-svid-B" } // 交替占槽
return s.km.GenerateKey(ctx, keyID, ECP256)
}这个 ForSVID 包装器(svidkeymanager.go:20)在节点认证 loadSVID(attestor/node/node.go:106)时被用来生成/恢复 agent SVID 的密钥。
WorkloadAttestor 插件(重点)
做什么
这是 agent"下发环节"的判定核心。当工作负载通过 Workload API 索要 SVID 时,agent 从 peertracker 拿到调用进程的 PID,交给 WorkloadAttestor 现算一组 selectors(如 unix:uid:1000、k8s:ns:default);再用这组 selectors 去匹配缓存中"本 agent 有权代理的注册条目",把命中的、早已预签好的 SVID 递给调用者。约束是 AtLeastOne——至少配一个;所有配置的插件会被并行调用,selectors 聚合。
Go 接口
// pkg/agent/plugin/workloadattestor/workloadattestor.go:11
type WorkloadAttestor interface {
catalog.PluginInfo
Attest(ctx context.Context, pid int) ([]*common.Selector, error) // PID 型
AttestReference(ctx context.Context, reference *anypb.Any) ([]*common.Selector, error) // 引用型(如 K8s 对象)
}内置实现与产出 selectors
| 插件名 | 判定依据 | 产出 selector(前缀即插件名) | 关键位置 |
|---|---|---|---|
| unix | /proc 进程属性 | uid、user、gid、group、supplementary_gid、supplementary_group;可选 path、sha256 | unix/unix_posix.go:133 |
| docker | Docker/Podman 容器 inspect | label:<k>:<v>、env:<E>、image_id、image_config_digest;可选 sigstore | docker/docker.go:128 |
| k8s | 经 kubelet 把 PID 映射到 Pod | ns、sa、node-name、pod-uid、pod-name、pod-label:<k>:<v>、pod-owner、container-name、container-image 等 | k8s/k8s.go:1712 |
| systemd | D-Bus 查 unit | id、fragment_path | systemd/systemd_posix.go:58 |
| windows | Windows 进程 token | user_name、user_sid、group_sid、group_name;可选 path、sha256 | windows/windows_windows.go:76 |
出处:pkg/agent/catalog/workloadattestor.go:29-37。
selector 示例(工作负载的一次请求可能同时命中多种):
unix:uid:1000
unix:user:app
unix:gid:1000
unix:path:/usr/local/bin/server
docker:label:com.example.team:frontend
docker:image_id:nginx:1.27
k8s:ns:default
k8s:sa:frontend
k8s:pod-label:app:web
systemd:id:myapp.service配置要点
- unix:
discover_workload_path(bool,默认 false)开启后额外产出path;workload_size_limit(int64)≥0 时进一步算sha256(读可执行文件计算摘要,需 agent 有权访问/proc/<pid>/exe)。 - docker:
docker_socket_path、docker_version、container_id_cgroup_matchers、podman_socket_path(支持 Podman)、sigstore {}(镜像签名验证)。 - k8s:
kubelet_read_only_port或kubelet_secure_port(二选一)、kubelet_ca_path、token_path、skip_kubelet_verification等,详见 K8S Agent 详解。 - systemd:无配置(直接连系统 D-Bus)。
- windows:
discover_workload_path、workload_size_limit、disable_group_name_selectors。
关键代码走读:并行聚合
聚合器 attestor/workload/workload.go 拿到所有 WorkloadAttestor 插件,并行(每插件一个 goroutine)对同一 PID 调 Attest,结果汇入 channel:
// pkg/agent/attestor/workload/workload.go:112
plugins := wla.c.Catalog.GetWorkloadAttestors()
sChan := make(chan []*common.Selector, len(plugins))
errChan := make(chan error, len(plugins))
var wg sync.WaitGroup
for _, p := range plugins {
wg.Go(func() {
if selectors, err := attestFunc(p); err == nil { sChan <- selectors } else { errChan <- err }
})
}
defer wg.Wait()
// 收集:成功则 append 到 selectors;某插件失败仅记日志、丢弃其 selectors;
// 只有当所有插件都失败(successes==0)时才返回合并错误(:163)容错语义很关键(workload.go:56 的注释与 :163 的逻辑):个别插件失败会被记录并丢弃其 selectors,但只要有一个成功就继续;仅当全部失败才向调用方报错。这样在混合环境(比如一个进程既不是容器也不在 k8s 里)不会因为 docker/k8s 插件"没找到"而阻断 unix 插件的结果。
selector 的类型前缀怎么来的:插件本身只返回值(如 unix 返回 uid:1000),由 V1 facade 统一贴上 Type = 插件名:
// pkg/agent/plugin/workloadattestor/v1.go:96
func (v1 *V1) selectorsFrom(values []string) []*common.Selector {
selectors := make([]*common.Selector, 0, len(values))
for _, value := range values {
selectors = append(selectors, &common.Selector{Type: v1.Name(), Value: value}) // Type=插件名
}
return selectors
}于是 unix 插件的 uid:1000 最终呈现为选择器 unix:uid:1000。V1 facade 还桥接了新旧两种调用风格(v1.go:29):优先调 PID 型 Attest,若插件返回 Unimplemented 则把 PID 打包成 WorkloadPIDReference 改走 AttestReference,反之亦然。
unix 插件的取值细节(unix/unix_posix.go:133)——注意 uid/gid 在有多个值(real/effective)时取 effective(索引 1):
// pkg/agent/plugin/workloadattestor/unix/unix_posix.go:150
uid, _ := p.getUID(proc) // getUID(:247):len>1 取 uids[1](effective)
selectorValues = append(selectorValues, makeSelectorValue("uid", uid))
if user, ok := p.getUserName(uid); ok { selectorValues = append(selectorValues, makeSelectorValue("user", user)) }
gid, _ := p.getGID(proc)
selectorValues = append(selectorValues, makeSelectorValue("gid", gid))
// ... supplementary_gid / supplementary_group 遍历附加组(:171)
if config.DiscoverWorkloadPath { // 需显式开启(:182)
processPath, _ := p.getPath(proc)
selectorValues = append(selectorValues, makeSelectorValue("path", processPath))
if config.WorkloadSizeLimit >= 0 {
sha256Digest, _ := util.GetSHA256Digest(exePath, config.WorkloadSizeLimit)
selectorValues = append(selectorValues, makeSelectorValue("sha256", sha256Digest))
}
}docker / systemd 的取值同理:docker 从容器 Config 提取 label/env/image(docker.go:221 getSelectorValuesFromConfig),再补 image_config_digest;systemd 通过 D-Bus GetUnitByPID(systemd_posix.go:120)拿到 unit 的 Id 与 FragmentPath。k8s 从 Pod 派生大量 selectors(k8s.go:1712 getSelectorValuesFromPodInfo,sa/ns/node-name/pod-uid/pod-name 起于 :1714),细节见 K8S Agent 详解。
这组 selectors 之后如何匹配 entry、取出预签的 SVID 并流式下发,见 SVID 颁发全流程 · 链路二。
SVIDStore 插件
做什么
有些工作负载不方便直接连 Workload API(如无 sidecar 的 Lambda、纯批处理任务、跨账户的托管服务)。SVIDStore 提供另一条路:把签发好的 SVID(证书链 + 私钥 + bundle)写入外部密钥库,工作负载再自行从密钥库读取。这类身份对应的注册条目带 -storeSVID 标志,不走 Workload API 下发。约束是 ZeroOrMore(可以一个都不配)。
Go 接口
// pkg/agent/plugin/svidstore/svidstore.go:13
type SVIDStore interface {
catalog.PluginInfo
DeleteX509SVID(ctx context.Context, metadata []string) error
PutX509SVID(context.Context, *X509SVID) error
}
type X509SVID struct { // :20
SVID *SVID
Metadata []string // 插件定位/命名密钥所需(来自 entry selectors)
FederatedBundles map[string][]*x509.Certificate
}
type SVID struct { SPIFFEID spiffeid.ID; CertChain []*x509.Certificate; PrivateKey crypto.PrivateKey; Bundle []*x509.Certificate; ExpiresAt time.Time } // :31内置实现
| 插件名 | 写入目标 | 配置 | 关键位置 |
|---|---|---|---|
| aws_secretsmanager | AWS Secrets Manager | access_key_id、secret_access_key、region | awssecretsmanager/aws.go:25 |
| gcp_secretmanager | GCP Secret Manager | service_account_file 等 | gcpsecretmanager/gcloud.go:27 |
出处:pkg/agent/catalog/svidstore.go:26-31。
配置要点:selectors 即"路由 + 元数据"
SVIDStore 的路由机制很特别——注册条目的 selector 类型就是目标插件名,selector 值就是给该插件的元数据。例如一条要存进 AWS 的条目,其 selectors 形如:
aws_secretsmanager:name:my-workload-svid
aws_secretsmanager:kmskeyid:alias/spire
aws_secretsmanager:region:us-west-2服务层 getStoreNameWithMetadata(svid/store/service.go:256)取 selectors[0].Type 作为插件名(并校验所有 selector 类型一致),把各 Value 收成 metadata 交给插件;插件再用 svidstore.ParseMetadata 把 metadata 解析成键值对(如 aws.go:256 取出 name/arn/kmskeyid)。
关键代码走读:独立的存储循环
带 -storeSVID 的条目在 agent 同步阶段就被分流到独立的 store 缓存,而非工作负载缓存(manager/sync.go:343):
// pkg/agent/manager/sync.go:343
for entryID, entry := range update.Entries {
switch {
case entry.StoreSvid: storeEntries[entryID] = entry // 分流到 SVIDStore 缓存
default: cacheEntries[entryID] = entry // 走常规工作负载缓存
}
}SVIDStoreService.Run(svid/store/service.go:82)是一个每 5 秒跑一轮的独立循环,把 store 缓存里"就绪待存"的记录逐条推给对应插件:
// pkg/agent/svid/store/service.go:135
func (s *SVIDStoreService) storeSVID(ctx, log, record *storecache.Record) {
storeName, metadata, _ := getStoreNameWithMetadata(record.Entry.Selectors) // selector[0].Type = 插件名
svidStore, ok := s.cat.GetSVIDStoreNamed(storeName) // 按名取插件
if !ok { log.Error("SVIDStore not found"); return }
req, _ := s.requestFromRecord(record, metadata) // 组装 X509SVID(链+私钥+bundle+联邦bundle)
if err := svidStore.PutX509SVID(ctx, req); err != nil { log.Error("Failed to put X509-SVID"); return }
s.cache.HandledRecord(record.Entry, record.Revision) // 标记已处理,避免重复写
}requestFromRecord(service.go:209)会把本域 rootCA、按 FederatesWith 收集的联邦 bundle、证书链与本地私钥一起打包。删除逻辑(deleteSVID,service.go:98)在条目被撤销或 selectors 变更时触发,先从密钥库删旧密钥再存新的(processRecords,service.go:175)。
与 Workload API 路径的本质区别:Workload API 是工作负载主动拉、agent 用 attestation 现场判权;SVIDStore 是 agent主动推到密钥库,鉴权发生在密钥库的 IAM/访问控制层。
-storeSVID条目的用法见 SVID 颁发全流程 · §8.4 的 entry 配置项表。
深入
- Server 详解 —— server 侧配对插件:NodeAttestor(验证证据、派生 ID)、KeyManager、UpstreamAuthority、DataStore、Notifier、BundlePublisher、CredentialComposer。
- K8S Agent 详解 —— Kubernetes 上的两个专属插件:节点认证
k8s_psat、工作负载认证k8s(经 kubelet 映射 Pod)。 - SVID 颁发全流程 —— 本文四类插件在"节点认证 → 工作负载认证 → CA 签名"三条链路里的代码级串联与端到端时序图。
- 整体架构 —— agent 在双组件架构中的位置、catalog 插件框架的内置/外部同构机制。
- 实战与部署 —— 把上述插件用起来:配置样例、上游 CA、生产落地。