SPIRE 整体架构源码分析
分析对象:SPIRE v1.15.2(SPIFFE Runtime Environment 官方实现,Go) 版本常量:
pkg/common/version/version.go:12(Base = "1.15.2")。 所有路径:行号,已用一手代码核对。 配套文档:SVID 颁发流程 · 高可用
0. 一句话架构
SPIRE 是 SPIFFE 标准的参考实现,由两个可独立部署的二进制组成,都构建在同一套插件化 catalog 框架之上:
- spire-server:信任域(trust domain)的信任根与控制面。持有/轮换 CA,维护注册表(registration entries + attested nodes),签发 X509/JWT/WIT SVID,对外暴露 SPIRE Server gRPC API 与 SPIFFE 联邦 bundle HTTPS 端点。
- spire-agent:每个工作负载节点上的本地守护进程。先向 server 完成节点认证换取自身 SVID,再周期同步"本节点被授权的注册项",本地缓存并轮换工作负载 SVID,通过 SPIFFE Workload API(Unix socket / named pipe)下发给本机工作负载。
一个贯穿全局的设计:对外 API 契约与插件契约都外置到上游模块,本仓库只做实现。
go.mod:go-spiffe/v2(Workload API 标准,:80)、spire-api-sdk(Server v1 API proto,:81)、spire-plugin-sdk(插件 gRPC 契约,:82)、hashicorp/go-plugin(进程外插件,:60)。
1. 顶层组件与启动流程
1.1 极薄的 main + 三段式启动
两个组件的 main.go 几乎对称,只把 CLI 的 Run 包进跨平台 EntryPoint:
// cmd/spire-server/main.go:10 (cmd/spire-agent/main.go:11 对称)
func main() {
os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main())
}run 子命令是唯一启动守护进程的命令,骨架是经典"三段式"(cmd/spire-server/cli/run/run.go:270):
c, err := LoadConfig(...) // ① 加载配置:parseFlags → ParseFile(HCL) → mergeInput → NewServerConfig
s := server.New(*c) // ② New:仅装配 Config(config.go:176)
err = s.Run(ctx) // ③ Run:依赖注入编排(见 1.2)server.New / agent.New 都极简,只包一层 Config。真正的装配在 Run 里。
1.2 server.run —— 顺序化依赖注入 + 并发 TaskRunner(核心)
Server.run(pkg/server/server.go:90)是整个 server 的装配中枢,按严格顺序创建并连线所有子系统,最后把长驻任务交给 TaskRunner 并发跑。装配顺序(server.go:90-299)简化:
telemetry → host services(identityprovider/agentstore)→ loadCatalog(加载插件)
→ bundle cache/pubmanager → validateTrustDomain
→ credBuilder(credtemplate) → credValidator(credvalidator)
→ newCA(ca.ServerCA) → newCAManager(ca/manager) → newCASync(ca/rotator)
→ newSVIDRotator(server 自身 SVID)→ authpolicy 引擎 → newBundleManager(联邦)
→ newEndpointsServer(组装 API 端点)→ host services SetDeps → newRegistrationManager然后并发运行长驻任务:
// pkg/server/server.go:264
tasks := []func(context.Context) error{
caSync.Run, // CA 轮换
svidRotator.Run, // server 自身 SVID 轮换
endpointsServer.ListenAndServe,// API 端点(TCP mTLS + UDS)
metrics.ListenAndServe,
bundleManager.Run, // 联邦 bundle 拉取
registrationManager.Run, // 注册项清理
bundlePublishingManager.Run, // bundle 发布到 BundlePublisher 插件
catalog.ReconfigureTask(...), // 插件动态重配
}
// 可选:配置了 PruneAttestedNodesExpiredFor 时追加 nodeManager.Run无 DI 框架——全靠
server.go:366-530的一组newXxx工厂手工连线。想理解"谁依赖谁",读这段即可(如newEndpointsServer把 catalog、svidRotator、serverCA、caManager、authPolicy、bundleManager 全注入endpoints.Config)。
1.3 agent.Run —— 多了一段"入网引导"
Agent.Run(pkg/agent/agent.go:63)比 server 多一段认证引导循环:
storage.Open(本地持久化)→ telemetry → catalog.Load(加载插件)
→ 选 NodeAttestor(join token 用内置,否则用插件)
→ 【引导循环 agent.go:190-261】加载 bootstrap bundle → a.attest(节点认证)→ 失败 backoff 重试
→ newManager(SVID 缓存与轮换)→ workload_attestor → newEndpoints(Workload API)
→ 并发:metrics / mgr.Run / storeService.Run / agentEndpoints.ListenAndServe / catalog 重配a.attest(agent.go:377)是 agent "入网"的关键一跳,落到 node attestor 子系统(详见 SVID 颁发流程 链路一)。
2. Server 侧内部模块(pkg/server/)
签发链贯穿这些模块:API/Rotator → ca.ServerCA → credtemplate.Builder 构模板 → 签名 → credvalidator.Validator 校验。
| 模块 | 职责 | 关键定位 |
|---|---|---|
ca/ | CA 证书/令牌签发(核心)。ServerCA 接口 + CA 实现 | 接口 ca.go:33;CA ca.go:167;签名主干见 SVID 颁发流程 §4 |
ca/manager | CA 密钥**双槽(current/next)**生命周期:prepare/activate/rotate;CA journal 持久化 | manager.go:258(Prepare)/:327(Activate)/:334(Rotate) |
ca/rotator | 定时器驱动 manager 做 Prepare/Activate | rotator.go:85(Run) |
endpoints/ | 组装并运行 Server 全部 gRPC API(双监听器,见 §5.2) | endpoints.go:249(ListenAndServe)/:305(TCP)/:324(UDS) |
catalog/ | 服务端插件目录,类型化访问插件 | 接口 catalog.go:50;Load :142 |
datastore/ | 唯一持久化抽象 DataStore;实现是内置 SQL | 接口 datastore.go:14;SQL sqlstore/sqlstore.go:105 |
credtemplate/ | 集中构造所有 X.509 模板与 JWT/WIT claims | Builder builder.go:136 |
credvalidator/ | 签发后(纵深防御)校验产物符合 SPIFFE 约束 | Validator validator.go:22 |
authorizedentries/ | 进程内多维索引快照,快速算"某 agent 可见的 entry 集合",避免每次打库 | Cache cache.go:35;LookupAuthorizedEntries :63 |
svid/ | Server 自身 SVID 轮换(用于 TCP mTLS 端点),Observer 发布状态 | rotator.go:20;observer.go:6 |
bundle/ | 三子包:client(拉联邦 bundle)/pubmanager(发布本地 bundle 到插件)/datastore(bundle 变更回调) | client/manager.go:86;pubmanager/pubmanager.go:26 |
registration/ | 注册项清理守护(每 5 分钟 prune 过期 entry) | manager.go:49(Run) |
node/ | attested node 清理守护(默认 ~1h + 抖动) | manager.go:67(Run) |
cache/ | 三类读缓存:entrycache(全量快照)/dscache(bundle 1s TTL)/nodecache(节点 5s) | entrycache/fullcache.go:91 等 |
3. Agent 侧内部模块(pkg/agent/)
装配链:agent.go 顶层 → catalog → node attestor(入网)→ manager(缓存与轮换,内部创建 svid.Rotator 与 client)→ endpoints(Workload API)。
| 模块 | 职责 | 关键定位 |
|---|---|---|
manager/ | agent 中枢:周期同步授权条目+bundle,维护工作负载 SVID 的 LRU 内存缓存,驱动签发/轮换,向 Workload API 提供订阅与查询 | 接口 manager.go:44;Run :230;synchronize sync.go:102 |
attestor/node | agent 入网:走 AttestAgent 双向流,经 NodeAttestor 插件应答挑战,换回 agent SVID + bundle | node.go:65(Attest) |
attestor/workload | 对给定 PID 并发调所有 WorkloadAttestor 插件,聚合 selectors | workload.go:27(接口)/:112(attest) |
endpoints/ | 本地(UDS/named pipe)gRPC server:Workload API + Envoy SDS v3 + Health | endpoints.go:45(New)/:110(ListenAndServe) |
svid/ | agent 自身 SVID 轮换,按 Reattestable 决定 reattest 或续签 | rotator.go:211(rotateSVIDIfNeeded) |
client/ | 到 server 的 mTLS gRPC 连接(引用计数复用),四个类型化子客户端 | 接口 client.go:91;dial.go:47 |
catalog/ | agent 端插件目录(4 类:KeyManager/NodeAttestor/SVIDStore/WorkloadAttestor) | 接口 catalog.go:31 |
storage/ | agent SVID/bundle/bootstrap 状态 JSON 落盘,重启后恢复 | 接口 storage.go:22;Open :50 |
4. 插件化 catalog 机制(核心扩展点)
4.1 内置与外部插件完全同构——都走 gRPC
通用框架在 pkg/common/catalog/。分叉点在 loadPlugin(catalog.go:319),判据是配置里是否给了可执行文件路径(HCL 的 plugin_cmd):
// pkg/common/catalog/catalog.go:320
if pluginConfig.IsExternal() {
return loadExternal(...) // 外部:hashicorp/go-plugin 子进程
}
// 否则匹配内置插件名 → loadBuiltIn(进程内)关键设计:内置插件也走 gRPC,只是经内存管道回环。loadBuiltIn(builtin.go:44)在进程内起 gRPC server,用 newPipeNet() + grpc.WithContextDialer 建进程内连接。于是内置与外部插件对上层是同构的 gRPC 客户端,上层代码对二者无感知。
外部插件走 go-plugin 子进程(external.go:74):用插件类型做握手 cookie,AllowedProtocols: [gRPC],可选 plugin_checksum(SHA256 校验二进制),Linux 下设 Pdeathsig: SIGKILL 防残留。host services 通过 go-plugin broker 反向暴露给子进程。
4.2 全部插件类型清单(v1.15.2)
| 插件类型 | 端 | 数量约束 | 内置实现 |
|---|---|---|---|
| NodeAttestor | server + agent | server ZeroOrMore / agent ExactlyOne | aws_iid, azure_msi/imds, gcp_iit, http_challenge, join_token, k8s_psat, sshpop, tpm_devid, x509pop |
| KeyManager | server + agent | ExactlyOne | server: aws_kms, gcp_kms, azure_keyvault, hashicorp_vault, disk, memory / agent: disk, memory |
| UpstreamAuthority | server | MaybeOne | aws_secret, aws_pca, gcp_cas, vault, spire, disk, cert_manager, ejbca |
| WorkloadAttestor | agent | AtLeastOne | docker, k8s, systemd, unix, windows |
| SVIDStore | agent | ZeroOrMore | aws_secretsmanager, gcp_secretmanager |
| Notifier | server | ZeroOrMore | gcs_bundle, k8s_bundle |
| BundlePublisher | server | ZeroOrMore | aws_s3, gcp_cloudstorage, aws_rolesanywhere, k8s_configmap |
| CredentialComposer | server | ZeroOrMore | uniqueid |
| DataStore | server(特例) | 恰好 1,仅内置 sql | sql(mysql/postgres/sqlite) |
出处:server 类型常量 pkg/server/catalog/catalog.go:39-45;agent pkg/agent/catalog/catalog.go:22-27;各 repo 的 BuiltIns()。
两个必须纠正的事实(相对旧版本/常见误解):
- DataStore 是刻意的例外:它被
FilterByType从通用加载中剥离并直连内置 SQL(catalog.go:162),以绕过 gRPC 消息大小限制;外部 DataStore 已废弃——catalog.go:265:"pluggability for the DataStore is deprecated; only the built-in ... plugin is supported"。NodeResolver在 v1.15.2 已被完全移除(全仓库 grep 零命中)。旧文档里的这个插件类型已不存在,其职责并入 attestation。
4.3 插件接口三层结构
每类插件:Go facade 接口(<name>.go)+ gRPC facade 适配(v1.go)+ repository(repository.go)。例:
// pkg/server/plugin/nodeattestor/nodeattestor.go:10
type NodeAttestor interface {
catalog.PluginInfo
Attest(ctx, payload []byte, challengeFn func(ctx, challenge []byte) ([]byte, error)) (*AttestResult, error)
}v1.go 把 spire-plugin-sdk 生成的 gRPC stub 适配成上面的 Go 接口。本仓库 proto/ 只保留三份内部 proto:common.proto(共享类型)、plugin.proto(Init/Configure)、private/.../journal.proto(CA journal)。
5. 对外 API 面
对外面共三类:①Server 管控面 gRPC;②Server 联邦面 HTTPS;③Agent 面 gRPC。核心结论:v1 API 的 proto 契约全部外置到 spire-api-sdk/go-spiffe,本仓库只实现接口,无 REST 网关。
5.1 SPIRE Server API(pkg/server/api/)—— 9 个 v1 gRPC 服务
| 服务 | 职责 | 关键 RPC |
|---|---|---|
Agent(agent/v1) | 节点生命周期 | AttestAgent(流,service.go:294)、RenewAgent(:423)、PostStatus(:491) |
SVID(svid/v1) | 签发/铸造 SVID 及下游 CA | BatchNewX509SVID(:193)、NewJWTSVID(:377)、BatchNewWITSVID(:424)、NewDownstreamX509CA(:603)、MintX509SVID(:72) |
Entry(entry/v1) | 注册项 CRUD;agent 拉授权项 | GetAuthorizedEntries 等 |
Bundle(bundle/v1) | 本地+联邦 bundle 管理 | GetBundle 等 |
TrustDomain(trustdomain/v1) | 联邦关系管理 | |
LocalAuthority(localauthority/v1) | 本地 CA 生命周期 prepare/activate/rotate/taint/revoke | |
| Debug / Health / Logger | 调试 / 健康检查 / 运行时日志级别 | 仅 UDS |
5.2 传输 vs 授权:双监听器 + 方法级 OPA 策略
endpoints.ListenAndServe 同起两个 gRPC server(endpoints.go:253-254):
- TCP server(
:305):grpc.Creds(TLS)—— mTLS,面向远程 agent、下游 CA、联邦 server、持 admin SVID 的远程管理员。证书源是 server 自身 SVID,信任源是 bundle。 - UDS server(
:324):peertracker本机 Unix 域套接字,面向本地 admin CLI。Logger/Health/Debug仅挂 UDS。
但**"agent 面 vs 管理员面"不由传输决定,而由逐 RPC 方法的 OPA 策略决定**(执行点 api/middleware/authorization.go:18,策略数据 authpolicy/policy_data.json)。实测分档计数:
allow_local : 53 仅本机 UDS(Debug/Logger/Health + 大量 admin CRUD)
allow_admin : 46 admin SVID / 本机
allow_agent : 8 已鉴权 agent(BatchNewX509SVID/NewJWTSVID/GetAuthorizedEntries/RenewAgent/PostStatus…)
allow_downstream: 3 下游 CA(NewDownstreamX509CA/PublishJWTAuthority/PublishWITAuthority)
allow_any : 2 免鉴权引导入口:Bundle/GetBundle 与 Agent/AttestAgent
allow_any的两个方法(GetBundle、AttestAgent)正是 agent "零信任冷启动" 的入口——未持 SVID 时也能取信任根并发起认证。
5.3 联邦面 HTTPS(pkg/server/endpoints/bundle/)
一个独立 HTTPS 服务(非 gRPC),GET / 返回本信任域 SPIFFE trust bundle(JSON/JWKS),供跨信任域联邦。三种认证:SPIFFEAuth(默认,SPIFFE mTLS)/ ACMEAuth(Web PKI)/ DiskCertManager。详见 高可用 §5。
5.4 Agent 面 gRPC(over UDS)
单一 gRPC server(peertracker 凭据),注册三服务:SPIFFE Workload API(workload/handler.go)、Envoy SDS v3(sdsv3/handler.go,给 Envoy 下发证书)、标准 gRPC Health。工作负载鉴权不用 mTLS,而是 peertracker 拿调用进程属性 + Attestor.Attest 现算 selectors(详见 SVID 颁发流程 链路二)。
6. 整体架构图与数据流
6.1 组件与插件挂载点
┌──────────────────────────── 工作负载节点(Node)────────────────────────────┐
│ Workload ──SPIFFE Workload API(UDS,peertracker+attest)──► spire-agent │
│ (Envoy) ───Envoy SDS v3────────────────────────────────► │
│ ┌──────────────────────── spire-agent ───────────────────────────────┐ │
│ │ endpoints(Workload API + SDS) │ │
│ │ manager(LRU SVID 缓存 + 轮换) │ │
│ │ ├ svid.Rotator(agent 自身 SVID) │ │
│ │ └ client(mTLS gRPC → server,引用计数) │ │
│ │ storage(SVID/bundle/bootstrap 落盘) │ │
│ │ ── 插件挂载点 ── NodeAttestor* │ WorkloadAttestor* │ KeyManager │ SVIDStore* │
│ └───────────────────────────────┬──────────────────────────────────────┘ │
└──────────────────────────────────┼───────────────────────────────────────────┘
│ gRPC/mTLS(TCP)
AttestAgent(流)/ SyncAuthorizedEntries / BatchNewX509SVID / RenewAgent
▼
┌──────────────────────────── spire-server ──────────────────────────────────┐
│ endpoints: TCP(mTLS,面向 agent) + UDS(本地 admin CLI) │
│ └─逐方法 OPA 授权(allow_agent/admin/local/downstream/any) │
│ API(v1): Agent │ SVID │ Entry │ Bundle │ TrustDomain │ LocalAuthority ... │
│ ├─► ca.ServerCA ── credtemplate(构模板)─► 签名 ─► credvalidator(校验) │
│ │ ▲ 密钥槽由 ca/manager(current/next)+ ca/rotator 轮换 │
│ │ └── KeyManager 插件(签名密钥)/ UpstreamAuthority 插件(上游CA)│
│ ├─► authorizedentries.Cache / cache/*(授权项与节点缓存) │
│ ├─► catalog ──► NodeAttestor* / Notifier* / CredentialComposer* │
│ └─► datastore.DataStore ──(直连,非 gRPC)──► 内置 SQL(mysql/pg/sqlite) │
│ 联邦面:endpoints/bundle(独立 HTTPS,GET / → trust bundle JSON) │
│ bundle/client(拉联邦 bundle) bundle/pubmanager ─► BundlePublisher* 插件 │
└─────────────────────────────────────────────────────────────────────────────┘
注:带 * 者为可替换插件挂载点;内置插件走进程内内存管道 gRPC,外部插件走 go-plugin 子进程。6.2 端到端数据流(工作负载申请 SVID)
阶段一/二/三的代码级细节见 SVID 颁发流程。
7. 架构要点总结
- 两个薄二进制 + 一个厚编排:
main→EntryPoint→cli→run都极薄,真正装配在server.run(server.go:90)与agent.Run(agent.go:63)里靠顺序化依赖注入 + TaskRunner 并发完成,无 DI 框架。 - 一切皆插件,且内置/外部同构:通用 catalog 让内置插件也走(内存管道)gRPC,外部插件走 go-plugin 子进程,上层对二者无感知;插件契约外置到
spire-plugin-sdk。 - DataStore 是刻意的例外:为绕过 gRPC 消息大小限制而直连内置 SQL,v1.15.2 已彻底关闭其可插拔性。
- 签发链职责清晰:
ServerCA只做编排,模板构造(credtemplate)与产物校验(credvalidator)拆成独立模块,签名密钥来自 KeyManager 插件,信任根可由 UpstreamAuthority 插件链到企业 PKI。 - 授权是方法级、数据驱动的:传输层(TCP mTLS / 本地 UDS)只是身份来源,真正权限判定由 OPA 策略按 RPC 方法给出 agent/admin/local/downstream/any 五档。
- 性能靠多级缓存:server 侧
authorizedentries.Cache+cache/{entrycache,dscache,nodecache}把授权计算与节点校验挡在 DB 之前;agent 侧manager的 LRU 缓存把工作负载 SVID 挡在 server 之前。