spire-server 详解
spire-server 是信任域的信任根与控制面:持有并轮换 CA、维护注册表、签发 SVID,并把 CA 签名密钥、上游信任根、节点认证、bundle 分发等一切"会因环境而变"的能力,统统外置为可插拔插件。本页从源码层面逐个拆解 spire-server 的插件框架与七类服务端插件。
分析对象:SPIRE v1.15.2(
pkg/common/version/version.go:12,Base = "1.15.2")。所有路径:行号相对spire-src,已用一手代码核对。 配套:整体架构 · SVID 颁发流程 · 高可用
Server 职责与装配
Server 结构体极简,只包一层 Config(pkg/server/server.go:63)。真正的装配在 Server.run(pkg/server/server.go:90)里靠顺序化依赖注入完成——插件是这条装配链的第一环:先 loadCatalog 把所有插件加载起来,后续所有子系统(CA、endpoints、bundle 管理器等)都从 catalog 里取插件句柄。
装配链中与插件直接相关的几处(server.go):
| 步骤 | 位置 | 说明 |
|---|---|---|
loadCatalog | server.go:176 | 加载全部插件,返回 *catalog.Repository(见下节) |
bundleCache = NewCache(DataStore) | server.go:181 | bundle 读缓存直接建在 DataStore 插件之上 |
newBundlePublishingManager | server.go:183 | 用 cat.BundlePublishers 建 bundle 发布器 |
WithBundleUpdateCallback | server.go:187 | 给 DataStore 包一层回调,bundle 变更时通知 BundlePublisher |
newCredBuilder | server.go:366 | 把 cat.GetCredentialComposers() 注入模板构造器(server.go:376) |
newCA / newCAManager | server.go:387 / :400 | CA 编排器与生命周期管理器,内部用 KeyManager 与 UpstreamAuthority 插件 |
newEndpointsServer | server.go:472 | 组装 gRPC API,catalog 整体注入 endpoints.Config(server.go:478) |
装配完成后,长驻任务交给 TaskRunner 并发运行(server.go:264),其中 bundlePublishingManager.Run 与 catalog.ReconfigureTask(插件动态重配)是插件相关的两个:
// pkg/server/server.go:264
tasks := []func(context.Context) error{
caSync.Run, // CA 轮换(KeyManager/UpstreamAuthority)
svidRotator.Run,
endpointsServer.ListenAndServe,
metrics.ListenAndServe,
bundleManager.Run,
registrationManager.Run,
bundlePublishingManager.Run, // 推 bundle 到 BundlePublisher 插件
catalog.ReconfigureTask(s.config.Log.WithField(telemetry.SubsystemName, "reconfigurer"), cat),
}想理解"谁依赖哪个插件",读
server.go:366-530的一组newXxx工厂即可——它们把 catalog 里的插件句柄手工连线进各子系统,无 DI 框架。整体启动流程见 整体架构 §1。
插件框架
通用插件框架在 pkg/common/catalog/,server 与 agent 共用。核心抽象是三个接口(pkg/common/catalog/catalog.go:18-59):Repository(按类型返回 PluginRepo)、PluginRepo(声明 Constraints() 数量约束 + BuiltIns() 内置清单)、Facade(把 gRPC stub 适配成 Go 接口)。
内置与外部插件完全同构——都走 gRPC
分叉点在 loadPlugin(pkg/common/catalog/catalog.go:319),判据是配置里是否给了外部可执行文件路径:
// pkg/common/catalog/catalog.go:319
func loadPlugin(ctx context.Context, builtIns []BuiltIn, pluginConfig PluginConfig, pluginLog logrus.FieldLogger, hostServices []pluginsdk.ServiceServer) (*pluginImpl, error) {
if pluginConfig.IsExternal() {
return loadExternal(ctx, externalConfig{ /* ... */ }) // 外部:go-plugin 子进程
}
for _, builtIn := range builtIns {
if pluginConfig.Name == builtIn.Name {
return loadBuiltIn(ctx, builtIn, BuiltInConfig{ /* ... */ }) // 内置:进程内
}
}
return nil, fmt.Errorf("no built-in plugin %q for type %q", pluginConfig.Name, pluginConfig.Type)
}内置插件也走 gRPC,只是经内存管道回环。loadBuiltIn(pkg/common/catalog/builtin.go:44)在进程内起一个 gRPC server,再用 newPipeNet() + grpc.WithContextDialer 拨号建立进程内连接(builtin.go:125-159):
// pkg/common/catalog/builtin.go:144
// Dial the server
conn, err := grpc.NewClient(
"passthrough:IGNORED",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(pipeNet.DialContext),
)于是内置与外部插件对上层都是同构的 gRPC 客户端 facade,上层代码对二者无感知。
外部插件走 hashicorp/go-plugin 子进程(pkg/common/catalog/external.go:43):用插件类型做握手 cookie(external.go:74-79),只允许 gRPC 协议(AllowedProtocols: [ProtocolGRPC],external.go:84),可选 plugin_checksum 对二进制做 SHA256 校验(external.go:58-66,buildSecureConfig :188)。host services(如 identityprovider、agentstore)通过 go-plugin broker 反向暴露给子进程。
加载流程与数量约束
catalog.Load(catalog.go:134)遍历配置逐个 loadPlugin → bindRepos(绑定到类型化仓库)→ configurePlugin(下发 HCL 配置),最后统一校验数量约束(catalog.go:208):
// pkg/common/catalog/catalog.go:208
for pluginType, pluginRepo := range pluginRepos {
if err := pluginRepo.Constraints().Check(pluginCounts[pluginType]); err != nil {
return nil, fmt.Errorf("plugin type %q constraint not satisfied: %w", pluginType, err)
}
}约束由四个工厂函数表达(pkg/common/catalog/constraints.go:7-21),Check 在 :33:
| 约束 | {Min, Max} | 语义 | 用在 |
|---|---|---|---|
ExactlyOne() | {1, 1} | 恰好一个 | KeyManager |
MaybeOne() | {0, 1} | 最多一个 | UpstreamAuthority |
AtLeastOne() | {1, 0} | 至少一个 | (agent 侧 WorkloadAttestor) |
ZeroOrMore() | {0, 0} | 任意个(含零) | NodeAttestor / Notifier / BundlePublisher / CredentialComposer |
server 插件类型常量与 Catalog 接口
server 端类型常量在 pkg/server/catalog/catalog.go:38-46,类型化访问接口 Catalog 在 :50-58:
// pkg/server/catalog/catalog.go:38
const (
bundlePublisherType = "BundlePublisher"
credentialComposerType = "CredentialComposer"
dataStoreType = "DataStore"
keyManagerType = "KeyManager"
nodeAttestorType = "NodeAttestor"
notifierType = "Notifier"
upstreamAuthorityType = "UpstreamAuthority"
)注意:
Repository.Plugins()(catalog.go:101)返回的 map 不含 DataStore——它被刻意剥离出通用加载流程(见 DataStore 节)。
server 端七类插件总览(全部以各 BuiltIns() 实测为准):
| 插件类型 | Go 接口 | 数量约束 | 内置实现数 | 走 gRPC? |
|---|---|---|---|---|
| NodeAttestor | nodeattestor/nodeattestor.go:10 | ZeroOrMore | 10 | 是 |
| KeyManager | keymanager/keymanager.go:17 | ExactlyOne | 6 | 是 |
| UpstreamAuthority | upstreamauthority/upstreamauthority.go:13 | MaybeOne | 8 | 是 |
| Notifier | notifier/notifier.go:10 | ZeroOrMore | 2 | 是 |
| BundlePublisher | bundlepublisher/bundleplublisher.go:10 | ZeroOrMore | 4 | 是 |
| CredentialComposer | credentialcomposer/credentialcomposer.go:13 | ZeroOrMore | 1 | 是 |
| DataStore(特例) | datastore/datastore.go:14 | 恰好 1 | 1(sql) | 否,直连 |
NodeAttestor
Go 接口(pkg/server/plugin/nodeattestor/nodeattestor.go:10):
// pkg/server/plugin/nodeattestor/nodeattestor.go:10
type NodeAttestor interface {
catalog.PluginInfo
Attest(ctx context.Context, payload []byte, challengeFn func(ctx context.Context, challenge []byte) ([]byte, error)) (*AttestResult, error)
}
type AttestResult struct {
AgentID string // 认证成功后分配给 agent 的 SPIFFE ID
Selectors []*common.Selector // 节点选择器(用于注册项匹配)
CanReattest bool // 该节点认证方式能否重复认证
}做什么:agent 入网时,server 侧用对应类型的 NodeAttestor 校验 agent 提交的节点身份凭证,产出 agent 的 SPIFFE ID 与节点选择器。约束是 ZeroOrMore(pkg/server/catalog/nodeattestor.go:26)——可配任意多个,server 按名字选用(Catalog.GetNodeAttestorNamed)。
内置实现表(BuiltIns() pkg/server/catalog/nodeattestor.go:36-49,共 10 个;配置名以名称常量实测为准):
| 配置名 | 目录 | 名称常量位置 | 机制 | 说明 |
|---|---|---|---|---|
join_token | jointoken | jointoken/join_token.go:17 | 预共享令牌 | 服务端内联处理,非挑战应答 |
aws_iid | awsiid | pkg/common/plugin/aws/iid.go:5 | 云 IID | 校验 AWS 实例身份文档 |
gcp_iit | gcpiit | gcpiit/iit.go:31 | 云 IIT | 校验 GCP 实例身份令牌 |
azure_msi | azuremsi | azuremsi/msi.go:36 | 云 MSI | 校验 Azure 托管身份令牌 |
azure_imds | azureimds | azureimds/imds.go:28 | 云 IMDS | 校验 Azure 实例元数据 |
k8s_psat | k8spsat | k8spsat/psat.go:25 | 令牌/K8s | K8s 投影 ServiceAccount 令牌(TokenReview 校验) |
x509pop | x509pop | x509pop/x509pop.go:34 | 挑战应答 | X.509 私钥持有证明 |
sshpop | sshpop | pkg/common/plugin/sshpop/sshpop.go:21 | 挑战应答 | SSH 私钥持有证明 |
tpm_devid | tpmdevid | pkg/common/plugin/tpmdevid/devid.go:5 | 挑战/TPM | TPM DevID 凭据 + 信用激活挑战 |
http_challenge | httpchallenge | httpchallenge/httpchallenge.go:26 | HTTP 挑战 | 反向 HTTP 挑战(证明控制某主机名) |
k8s_psat是 SPIRE on Kubernetes 的标准节点认证方式,细节见 K8S Agent 详解。
关键代码走读:挑战应答 vs join_token
绝大多数 NodeAttestor 是挑战应答式——V1 facade 驱动一个双向流循环:发 payload,若插件回 challenge 就调 challengeFn 取应答再发回,直到插件回 AgentAttributes(pkg/server/plugin/nodeattestor/v1.go:74-102):
// pkg/server/plugin/nodeattestor/v1.go:74
for {
resp, err := stream.Recv()
if err != nil {
return nil, v1.streamError(err)
}
if attribs = resp.GetAgentAttributes(); attribs != nil {
break // 拿到最终身份,结束
}
challenge := resp.GetChallenge()
if challenge == nil {
return nil, v1.Error(codes.Internal, "plugin response missing challenge or agent attributes")
}
response, err := challengeFn(ctx, challenge) // 回到 agent 侧求应答
if err != nil {
return nil, err
}
err = stream.Send(&nodeattestorv1.AttestRequest{
Request: &nodeattestorv1.AttestRequest_ChallengeResponse{ChallengeResponse: response},
})
// ...
}而 join_token 是特例:它的插件 Attest 直接返回 Unimplemented(pkg/server/plugin/nodeattestor/jointoken/join_token.go:58),因为"验令牌"这件事没有密码学挑战,SPIRE 干脆在 API 服务里内联处理:
// pkg/server/plugin/nodeattestor/jointoken/join_token.go:58
func (p *Plugin) Attest(nodeattestorv1.NodeAttestor_AttestServer) error {
return status.Error(codes.Unimplemented, "join token attestation is currently implemented within the server")
}对应的服务端分流在 pkg/server/api/agent/v1/service.go:320:type == "join_token" 走 attestJoinToken(:654,从 DataStore 取出并删除一次性令牌);否则走 attestChallengeResponse(:683,调 GetNodeAttestorNamed(...).Attest(...) 进入上面的挑战循环)。也正因如此,Load 里硬性禁止用外部插件覆盖内置 join_token(pkg/server/catalog/catalog.go:143)。
KeyManager
Go 接口(pkg/server/plugin/keymanager/keymanager.go:17):
// pkg/server/plugin/keymanager/keymanager.go:17
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)
}
// Key 内嵌 crypto.Signer——私钥永远不出插件,只暴露"签名"能力
type Key interface {
crypto.Signer
ID() string
}做什么:为 server CA 保管签名私钥并执行签名。私钥可以在内存、落盘,也可以托管在云 KMS / HSM——Key 只暴露 crypto.Signer,签名动作发生在插件内,私钥材料对 SPIRE 主体不可见。支持的密钥类型是 ec-p256 / ec-p384 / rsa-2048 / rsa-4096(keymanager.go:43-49)。约束 ExactlyOne(pkg/server/catalog/keymanager.go:23)——必须且只能配一个。
内置实现表(BuiltIns() pkg/server/catalog/keymanager.go:31-40,共 6 个):
| 配置名 | 目录 | 名称常量位置 | 密钥落点 | 备注 |
|---|---|---|---|---|
memory | memory | memory/memory.go:20 | 进程内存 | 重启即失效,仅测试/短命场景 |
disk | disk | disk/disk.go:32 | 本地磁盘 | 单机持久化 |
aws_kms | awskms | awskms/awskms.go:37 | AWS KMS | 私钥不出 KMS |
gcp_kms | gcpkms | gcpkms/gcpkms.go:39 | GCP KMS | 私钥不出 KMS |
azure_key_vault | azurekeyvault | azurekeyvault/azure_key_vault.go:38 | Azure KeyVault | 私钥不出 KeyVault |
hashicorp_vault | hashicorpvault | hashicorpvault/hashicorp_vault.go:29 | Vault Transit | 私钥不出 Vault |
配置要点:memory/disk 适合单机;多副本高可用部署下,若想让所有 server 副本共用同一 CA 签名密钥,应选云 KMS 类插件(密钥集中托管、天然共享)。KeyManager 装配后会被 telemetry 包一层(pkg/server/catalog/catalog.go:192)再交给 CA。密钥槽(current/next)的双槽轮换由 ca/manager 驱动,详见 高可用。
UpstreamAuthority
Go 接口(pkg/server/plugin/upstreamauthority/upstreamauthority.go:13):
// pkg/server/plugin/upstreamauthority/upstreamauthority.go:13
type UpstreamAuthority interface {
catalog.PluginInfo
// 把 CSR 送到上游 CA 铸造中间 CA;返回新 CA 链 + 上游 X.509 根 + 更新流
MintX509CA(ctx context.Context, csr []byte, preferredTTL time.Duration) (x509CA []*x509.Certificate, upstreamX509Authorities []*x509certificate.X509Authority, stream UpstreamX509AuthorityStream, err error)
// 把本地 JWT 公钥发布到上游;可选,不支持者返回 NotImplemented
PublishJWTKey(ctx context.Context, jwtKey *common.PublicKey) (jwtAuthorities []*common.PublicKey, stream UpstreamJWTAuthorityStream, err error)
// 订阅上游 bundle 变更以同步本地信任根;可选但强烈建议
SubscribeToLocalBundle(ctx context.Context) (x509CAs []*x509certificate.X509Authority, jwtAuthorities []*common.PublicKey, stream LocalBundleUpdateStream, err error)
}做什么:让 SPIRE 的 CA 不做自签名根,而是把 CSR 交给企业既有 PKI / 云 CA 铸造中间 CA,从而把 SPIRE 信任链挂到组织根信任之下。约束 MaybeOne(pkg/server/catalog/upstreamauthority.go:24)——最多一个,可不配(不配即自签根)。MintX509CA 返回的 stream 允许上游根轮换时推更新,是 无缝 CA 轮换 的机制基础。
内置实现表(BuiltIns() pkg/server/catalog/upstreamauthority.go:34-45,共 8 个):
| 配置名 | 目录 | 名称常量位置 | 上游 | PublishJWTKey |
|---|---|---|---|---|
disk | disk | disk/disk.go:38 | 本地磁盘 CA 证书/私钥 | 否(Unimplemented) |
spire | spire | spire/spire.go:30 | 上游 SPIRE Server | 是(唯一) |
aws_pca | awspca | awspca/pca.go:30 | AWS Private CA | 否 |
awssecret | awssecret | awssecret/awssecret.go:27 | AWS Secrets Manager 里的 CA 材料 | 否 |
gcp_cas | gcpcas | gcpcas/gcpcas.go:37 | GCP CA Service | 否 |
vault | vault | vault/vault.go:25 | HashiCorp Vault PKI | 否 |
cert-manager | certmanager | certmanager/certmanager.go:25 | K8s cert-manager Issuer | 否 |
ejbca | ejbca | ejbca/ejbca.go:35 | EJBCA | 否 |
只有
spire支持PublishJWTKey——经核对,其余 7 个插件的PublishJWTKeyAndSubscribe全部返回codes.Unimplemented(如awspca/pca.go:310、vault/vault.go:235、ejbca/ejbca.go:322)。原因见下面走读:发布 JWT 公钥需要上游本身理解 SPIFFE bundle 语义,只有"上游也是一台 SPIRE Server"时才成立。
关键代码走读:spire 插件为何独享 JWT 发布
spire 上游插件本质是一个"到上游 SPIRE Server 的客户端"。MintX509CA 落到上游的 NewDownstreamX509CA RPC(pkg/server/plugin/upstreamauthority/spire/spire.go:160):
// pkg/server/plugin/upstreamauthority/spire/spire.go:169
certChain, _, err := p.serverClient.newDownstreamX509CA(stream.Context(), request.Csr, request.PreferredTtl)PublishJWTKey 则落到上游的 PublishJWTAuthority RPC(spire.go:273)——普通 CA(磁盘、AWS PCA、Vault…)没有"接收并聚合 SPIFFE JWT 公钥"的概念,自然只能 Unimplemented:
// pkg/server/plugin/upstreamauthority/spire/spire.go:286
resp, err := p.serverClient.publishJWTAuthority(stream.Context(), jwtKey)
// ... 把返回的上游 JWT authorities 回流给本地 bundle配置要点:云上落地(AWS/GCP/Azure)通常选对应云 CA 插件,详见 云上落地;跨层级 SPIRE(nested SPIRE)选 spire;企业自有 PKI 选 vault/ejbca/cert-manager。轮换实践见 上游 CA 与无缝轮换。
DataStore
DataStore 是七类里的特例:它不走通用 gRPC 插件通道,而是被直连内置 SQL。
Go 接口(pkg/server/datastore/datastore.go:14)是 server 唯一的持久化抽象,方法覆盖 bundle、注册项(entry)、被认证节点(attested node)、节点选择器、join token、联邦关系、CA journal 及各自的 event 表:
// pkg/server/datastore/datastore.go:14 (节选)
type DataStore interface {
// Bundles
AppendBundle(context.Context, *common.Bundle) (*common.Bundle, error)
FetchBundle(ctx context.Context, trustDomainID string) (*common.Bundle, error)
// Entries
CreateRegistrationEntry(context.Context, *common.RegistrationEntry) (*common.RegistrationEntry, error)
ListRegistrationEntries(context.Context, *ListRegistrationEntriesRequest) (*ListRegistrationEntriesResponse, error)
// Nodes
CreateAttestedNode(context.Context, *common.AttestedNode) (*common.AttestedNode, error)
// ... 以及 Tokens / Federation Relationships / CA Journals
}为什么是特例:Load 一开始就用 FilterByType 把 DataStore 配置从通用插件列表里剥离,直接加载内置 SQL 插件,"以绕过 gRPC 的响应大小限制"(pkg/server/catalog/catalog.go:160-167)。外部 DataStore 已废弃——loadSQLDataStore(catalog.go:254)强制只接受内置 sql:
// pkg/server/catalog/catalog.go:264
if sqlConfig.Name != ds_sql.PluginName {
return nil, fmt.Errorf("pluggability for the DataStore is deprecated; only the built-in %q plugin is supported", ds_sql.PluginName)
}
if sqlConfig.IsExternal() {
return nil, fmt.Errorf("pluggability for the DataStore is deprecated; only the built-in %q plugin is supported", ds_sql.PluginName)
}内置实现:唯一插件名 sql(pkg/server/datastore/sqlstore/sqlstore.go:54),内部按 database_type 选方言,支持 sqlite / postgres / mysql 三种(sqlstore/sqlstore.go:1081):
// pkg/server/datastore/sqlstore/sqlstore.go:1085
switch {
case isSQLiteDbType(cfg.DBTypeConfig.DatabaseType):
dialect = sqliteDB{log: ds.log}
case isPostgresDbType(cfg.DBTypeConfig.DatabaseType):
dialect = postgresDB{}
case isMySQLDbType(cfg.DBTypeConfig.DatabaseType):
dialect = mysqlDB{logger: ds.log}
default:
return nil, "", false, nil, sqlcommon.NewSQLError("unsupported database_type: %v", cfg.DBTypeConfig.DatabaseType)
}加载完成后,DataStore 还会被叠上健康检查、telemetry 与 dscache(1s TTL 读缓存)三层包装(catalog.go:184-191)。
配置要点:单机/开发用 sqlite3;生产高可用用 postgres/mysql(可配只读副本 ro_connection_string 分流读),这是 SPIRE Server 无状态化、多副本水平扩展的前提,详见 高可用。
Notifier
Go 接口(pkg/server/plugin/notifier/notifier.go:10):
// pkg/server/plugin/notifier/notifier.go:10
type Notifier interface {
catalog.PluginInfo
NotifyAndAdviseBundleLoaded(ctx context.Context, bundle *common.Bundle) error // 首次加载:必须成功(advise)
NotifyBundleUpdated(ctx context.Context, bundle *common.Bundle) error // 后续更新:尽力而为
}做什么:在本信任域 bundle 首次加载与每次更新时收到回调,把 bundle 推送/落地到外部(如写进某个 K8s 资源)。约束 ZeroOrMore(pkg/server/catalog/notifier.go:18)。触发点在 CA manager:首次加载调 NotifyAndAdviseBundleLoaded(pkg/server/ca/manager/manager.go:696),CA 轮换致 bundle 变更时调 NotifyBundleUpdated(manager.go:893);无 notifier 时快速返回(manager.go:899)。
内置实现表(BuiltIns() pkg/server/catalog/notifier.go:28-33,共 2 个):
| 配置名 | 目录 | 名称/常量位置 | 用途 |
|---|---|---|---|
gcs_bundle | gcsbundle | gcsbundle/gcsbundle.go:33 | 把 bundle 写入 Google Cloud Storage 对象 |
k8sbundle | k8sbundle | k8sbundle/k8sbundle.go:52 | 把 bundle 同步进 K8s ConfigMap / Webhook / APIService 的 CA 字段 |
配置名以
MakeBuiltIn(...)实测为准:K8s 那个注册名是k8sbundle(一个词,非k8s_bundle)。k8sbundle是把 SPIRE 信任根喂给 K8s 准入 webhook / 联邦对端的常用手段。
BundlePublisher
Go 接口(pkg/server/plugin/bundlepublisher/bundleplublisher.go:10):
// pkg/server/plugin/bundlepublisher/bundleplublisher.go:10
type BundlePublisher interface {
catalog.PluginInfo
PublishBundle(ctx context.Context, bundle *common.Bundle) error
}做什么:与 Notifier 目标相近(对外分发本域 bundle),但驱动模型不同——BundlePublisher 由一个专门的 pubmanager 托管:既定时(30s)刷新,又在 bundle 变更时被 DataStore 回调唤醒后统一发布(pkg/server/bundle/pubmanager/pubmanager.go:61,refreshInterval :22):
// pkg/server/bundle/pubmanager/pubmanager.go:61
func (m *Manager) Run(ctx context.Context) error {
ticker := m.clock.Ticker(refreshInterval) // 30s
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.callPublishBundle(ctx) // 周期发布
case <-m.bundleUpdatedCh:
m.callPublishBundle(ctx) // 变更即发布
// ...
}
}
}bundleUpdatedCh 的唤醒源正是 server.go:187 给 DataStore 挂的 WithBundleUpdateCallback。约束 ZeroOrMore(pkg/server/catalog/bundlepublisher.go:20)。
内置实现表(BuiltIns() pkg/server/catalog/bundlepublisher.go:28-35,共 4 个):
| 配置名 | 目录 | 名称常量位置 | 落点 |
|---|---|---|---|
aws_s3 | awss3 | awss3/awss3.go:26 | AWS S3 对象 |
gcp_cloudstorage | gcpcloudstorage | gcpcloudstorage/gcpcloudstorage.go:26 | GCP Cloud Storage 对象 |
aws_rolesanywhere_trustanchor | awsrolesanywhere | awsrolesanywhere/awsrolesanywhere.go:24 | AWS IAM Roles Anywhere 信任锚 |
k8s_configmap | k8sconfigmap | k8sconfigmap/k8sconfigmap.go:24 | K8s ConfigMap |
配置名以实测为准:AWS Roles Anywhere 那个注册名是
aws_rolesanywhere_trustanchor(非aws_rolesanywhere)。
CredentialComposer
Go 接口(pkg/server/plugin/credentialcomposer/credentialcomposer.go:13):
// pkg/server/plugin/credentialcomposer/credentialcomposer.go:13
type CredentialComposer interface {
catalog.PluginInfo
ComposeServerX509CA(ctx context.Context, attributes X509CAAttributes) (X509CAAttributes, error)
ComposeServerX509SVID(ctx context.Context, attributes X509SVIDAttributes) (X509SVIDAttributes, error)
ComposeAgentX509SVID(ctx context.Context, id spiffeid.ID, publicKey crypto.PublicKey, attributes X509SVIDAttributes) (X509SVIDAttributes, error)
ComposeWorkloadX509SVID(ctx context.Context, id spiffeid.ID, publicKey crypto.PublicKey, attributes X509SVIDAttributes) (X509SVIDAttributes, error)
ComposeWorkloadJWTSVID(ctx context.Context, id spiffeid.ID, attributes JWTSVIDAttributes) (JWTSVIDAttributes, error)
}做什么:在签发各类凭据前,允许插件改写将要写入的属性(X.509 的 Subject/DNSNames/扩展、JWT 的 claims),是一个"证书模板定制"扩展点。它被注入模板构造器 credtemplate.Builder(pkg/server/server.go:376,CredentialComposers: cat.GetCredentialComposers()),在签名链的"构模板"阶段生效(签发主干见 SVID 颁发流程)。约束 ZeroOrMore(pkg/server/catalog/credentialcomposer.go:17)。
内置实现(BuiltIns() pkg/server/catalog/credentialcomposer.go:25,仅 1 个):
| 配置名 | 目录 | 名称/位置 | 用途 |
|---|---|---|---|
uniqueid | uniqueid | uniqueid/plugin.go:19 | 在工作负载 X.509-SVID 的 Subject 里加入 SPIFFE "unique ID" 属性 |
关键代码走读:uniqueid 只实现 ComposeWorkloadX509SVID(其余 Compose 方法一律 Unimplemented),把从 SPIFFE ID 派生的稳定 unique-ID 属性写入 Subject(pkg/server/plugin/credentialcomposer/uniqueid/plugin.go:47,派生逻辑 x509svid.UniqueIDAttribute,:95):
// pkg/server/plugin/credentialcomposer/uniqueid/plugin.go:66
// Add the attribute if it does not already exist. Otherwise, replace the old value.
found := false
for i := range len(attributes.Subject.ExtraNames) {
if attributes.Subject.ExtraNames[i].Oid == uniqueID.Oid {
attributes.Subject.ExtraNames[i] = uniqueID
found = true
break
}
}
if !found {
attributes.Subject.ExtraNames = append(attributes.Subject.ExtraNames, uniqueID)
}深入
- Agent 详解 —— agent 侧四类插件(NodeAttestor / WorkloadAttestor / KeyManager / SVIDStore)与入网/轮换
- K8S Agent 详解 ——
k8s_psat节点认证与 Kubernetes 集成 - SVID 颁发流程 —— CA 签名链如何串起 KeyManager / UpstreamAuthority / CredentialComposer
- 整体架构 —— 两个薄二进制 + 一个厚编排,以及 catalog 全景