Skip to content

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):

步骤位置说明
loadCatalogserver.go:176加载全部插件,返回 *catalog.Repository(见下节)
bundleCache = NewCache(DataStore)server.go:181bundle 读缓存直接建在 DataStore 插件之上
newBundlePublishingManagerserver.go:183cat.BundlePublishers 建 bundle 发布器
WithBundleUpdateCallbackserver.go:187给 DataStore 包一层回调,bundle 变更时通知 BundlePublisher
newCredBuilderserver.go:366cat.GetCredentialComposers() 注入模板构造器(server.go:376)
newCA / newCAManagerserver.go:387 / :400CA 编排器与生命周期管理器,内部用 KeyManager 与 UpstreamAuthority 插件
newEndpointsServerserver.go:472组装 gRPC API,catalog 整体注入 endpoints.Config(server.go:478)

装配完成后,长驻任务交给 TaskRunner 并发运行(server.go:264),其中 bundlePublishingManager.Runcatalog.ReconfigureTask(插件动态重配)是插件相关的两个:

go
// 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),判据是配置里是否给了外部可执行文件路径:

go
// 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):

go
// 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)遍历配置逐个 loadPluginbindRepos(绑定到类型化仓库)→ configurePlugin(下发 HCL 配置),最后统一校验数量约束(catalog.go:208):

go
// 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:

go
// 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?
NodeAttestornodeattestor/nodeattestor.go:10ZeroOrMore10
KeyManagerkeymanager/keymanager.go:17ExactlyOne6
UpstreamAuthorityupstreamauthority/upstreamauthority.go:13MaybeOne8
Notifiernotifier/notifier.go:10ZeroOrMore2
BundlePublisherbundlepublisher/bundleplublisher.go:10ZeroOrMore4
CredentialComposercredentialcomposer/credentialcomposer.go:13ZeroOrMore1
DataStore(特例)datastore/datastore.go:14恰好 11(sql)否,直连

NodeAttestor

Go 接口(pkg/server/plugin/nodeattestor/nodeattestor.go:10):

go
// 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_tokenjointokenjointoken/join_token.go:17预共享令牌服务端内联处理,非挑战应答
aws_iidawsiidpkg/common/plugin/aws/iid.go:5云 IID校验 AWS 实例身份文档
gcp_iitgcpiitgcpiit/iit.go:31云 IIT校验 GCP 实例身份令牌
azure_msiazuremsiazuremsi/msi.go:36云 MSI校验 Azure 托管身份令牌
azure_imdsazureimdsazureimds/imds.go:28云 IMDS校验 Azure 实例元数据
k8s_psatk8spsatk8spsat/psat.go:25令牌/K8sK8s 投影 ServiceAccount 令牌(TokenReview 校验)
x509popx509popx509pop/x509pop.go:34挑战应答X.509 私钥持有证明
sshpopsshpoppkg/common/plugin/sshpop/sshpop.go:21挑战应答SSH 私钥持有证明
tpm_devidtpmdevidpkg/common/plugin/tpmdevid/devid.go:5挑战/TPMTPM DevID 凭据 + 信用激活挑战
http_challengehttpchallengehttpchallenge/httpchallenge.go:26HTTP 挑战反向 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):

go
// 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 服务里内联处理:

go
// 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):

go
// 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 个):

配置名目录名称常量位置密钥落点备注
memorymemorymemory/memory.go:20进程内存重启即失效,仅测试/短命场景
diskdiskdisk/disk.go:32本地磁盘单机持久化
aws_kmsawskmsawskms/awskms.go:37AWS KMS私钥不出 KMS
gcp_kmsgcpkmsgcpkms/gcpkms.go:39GCP KMS私钥不出 KMS
azure_key_vaultazurekeyvaultazurekeyvault/azure_key_vault.go:38Azure KeyVault私钥不出 KeyVault
hashicorp_vaulthashicorpvaulthashicorpvault/hashicorp_vault.go:29Vault 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):

go
// 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
diskdiskdisk/disk.go:38本地磁盘 CA 证书/私钥否(Unimplemented)
spirespirespire/spire.go:30上游 SPIRE Server(唯一)
aws_pcaawspcaawspca/pca.go:30AWS Private CA
awssecretawssecretawssecret/awssecret.go:27AWS Secrets Manager 里的 CA 材料
gcp_casgcpcasgcpcas/gcpcas.go:37GCP CA Service
vaultvaultvault/vault.go:25HashiCorp Vault PKI
cert-managercertmanagercertmanager/certmanager.go:25K8s cert-manager Issuer
ejbcaejbcaejbca/ejbca.go:35EJBCA

只有 spire 支持 PublishJWTKey——经核对,其余 7 个插件的 PublishJWTKeyAndSubscribe 全部返回 codes.Unimplemented(如 awspca/pca.go:310vault/vault.go:235ejbca/ejbca.go:322)。原因见下面走读:发布 JWT 公钥需要上游本身理解 SPIFFE bundle 语义,只有"上游也是一台 SPIRE Server"时才成立。

关键代码走读:spire 插件为何独享 JWT 发布

spire 上游插件本质是一个"到上游 SPIRE Server 的客户端"。MintX509CA 落到上游的 NewDownstreamX509CA RPC(pkg/server/plugin/upstreamauthority/spire/spire.go:160):

go
// 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:

go
// 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 表:

go
// 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:

go
// 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):

go
// 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):

go
// 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_bundlegcsbundlegcsbundle/gcsbundle.go:33把 bundle 写入 Google Cloud Storage 对象
k8sbundlek8sbundlek8sbundle/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):

go
// 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):

go
// 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_s3awss3awss3/awss3.go:26AWS S3 对象
gcp_cloudstoragegcpcloudstoragegcpcloudstorage/gcpcloudstorage.go:26GCP Cloud Storage 对象
aws_rolesanywhere_trustanchorawsrolesanywhereawsrolesanywhere/awsrolesanywhere.go:24AWS IAM Roles Anywhere 信任锚
k8s_configmapk8sconfigmapk8sconfigmap/k8sconfigmap.go:24K8s ConfigMap

配置名以实测为准:AWS Roles Anywhere 那个注册名是 aws_rolesanywhere_trustanchor(非 aws_rolesanywhere)。


CredentialComposer

Go 接口(pkg/server/plugin/credentialcomposer/credentialcomposer.go:13):

go
// 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 个):

配置名目录名称/位置用途
uniqueiduniqueiduniqueid/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):

go
// 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 全景