Skip to content

SPIRE SVID 颁发全流程(代码级详解)

分析对象:SPIRE v1.15.2 所有代码引用格式为 相对路径:行号(相对仓库根),行号均经实际核对。 配套文档:整体架构 · 高可用


0. 先厘清:SVID 是什么(规范视角)

SVID = SPIFFE Verifiable Identity Document(可验证身份文档),是 SPIFFE 体系里"身份"的物理载体。一个 SVID 承载一个 SPIFFE ID:

spiffe://<trust-domain>/<path>      例:spiffe://example.org/ns/default/sa/frontend

SPIRE 颁发三种 SVID:

形态载体私钥位置典型用途默认 TTL
X509-SVIDX.509 证书Agent/Workload 本地mTLS 双向认证1h
JWT-SVID签名 JWTServer(KeyManager 后端)面向 OIDC/无 mTLS 通道的认证5m
WIT-SVIDJWT(Workload Identity Token)Server新增的轻量令牌形态

规范对 X509-SVID 的硬约束(来自 /spec/x509-svid),SPIRE 的证书模板完全遵循:

  • SPIFFE ID 必须写在 URI 类型的 SAN 里,且有且仅有一个 URI SAN(标准 §2);
  • 叶子证书 cA=false、必须置 digitalSignature(标准 §4.1/§4.3);
  • 签名证书(CA)cA=true、置 keyCertSign;
  • 叶子 SVID 的 EKU 应含 serverAuth+clientAuth(标准 §4.4),因此同一张证书可同时作 mTLS 的两端。

核心安全思想:主体不能自证身份。私钥在本地生成,但 CSR 里不含 SPIFFE ID,身份完全由 Server 依据"认证结果 / 注册条目"决定。下文三条链路会反复体现这一点。


1. 总览:三条颁发链路汇入一条签名主干

链路一  节点认证(Node Attestation)   → Agent 领取自己的 Agent SVID
链路二  工作负载认证(Workload Attest) → Workload 通过 Workload API 领取 SVID
链路三  CA 签名                        → 前两条最终都落到这里真正"签字"

三条链路最终都汇聚到 pkg/server/ca/ca.go 的签名主干 signX509SVID / signJWTSVID。签发入口一览:

SVID 类型应用层入口最终签名调用
Agent X509-SVIDpkg/server/api/agent/v1/service.go:627 signSvidca.SignAgentX509SVID (ca.go:308)
Workload X509-SVIDpkg/server/api/svid/v1/service.go:260 newX509SVIDca.SignWorkloadX509SVID (ca.go:335)
Workload JWT-SVIDpkg/server/api/svid/v1/service.go:332 mintJWTSVIDca.SignWorkloadJWTSVID (ca.go:365)

⚠️ 容易混淆的两个 "Manager",先分清:

组件路径职责
Agent 缓存 Managerpkg/agent/manager/manager.goAgent 侧维护 registration entries + workload SVID 缓存,周期同步
Server CA 生命周期 Managerpkg/server/ca/manager/manager.goServer 侧 prepare/activate/rotate CA 密钥槽,把签名密钥注入 CA 对象
Server CA 签名器pkg/server/ca/ca.go真正执行 SignXxxSVID 的对象,实现 ServerCA 接口

2. 链路一:节点认证 → Agent 领取自己的 SVID

Agent 启动时得先证明"我是谁",才能拿到一张代表自己的 Agent SVID(后续它才有资格代workload 向 Server 批量索要 SVID)。

2.1 Agent 侧:生成 attestation 数据并请求 SVID

入口 attestor.Attest(pkg/agent/attestor/node/node.go:65)。启动时先尝试从磁盘加载既有 SVID;没有才走节点认证:

go
// pkg/agent/attestor/node/node.go:84
switch {
case svid == nil:
    log.Info("SVID is not found. Starting node attestation")
    svid, bundle, reattestable, err = a.newSVID(ctx, key, bundle)   // 领取新 SVID

newSVID(node.go:218)组装请求。关键点:生成不带 URI SAN 的 CSR——SPIFFE ID 由 Server 决定:

go
// pkg/agent/attestor/node/node.go:229
csr, err := util.MakeCSRWithoutURISAN(key)     // CSR 里不放 SPIFFE ID
...
newSVID, reattestable, err := a.getSVID(ctx, conn, csr, a.c.NodeAttestor)
newBundle, err := a.getBundle(ctx, conn)       // 顺便拉取信任域 bundle

getSVID(node.go:189)构造 ServerStream 驱动 NodeAttestor 插件,插件产出的数据经 SendAttestationData(node.go:310)封装为请求,底层打开 gRPC 双向流 Client.AttestAgent(ctx)(node.go:336),循环接收"挑战"或"最终 SVID"。

2.2 Server 侧:验证 → 生成 SPIFFE ID → 签发 → 落库

入口 Service.AttestAgent(pkg/server/api/agent/v1/service.go:294),是一个 gRPC streaming handler。它按 attestation 类型二选一:

go
// pkg/server/api/agent/v1/service.go:318
if params.Data.Type == "join_token" {
    attestResult, err = s.attestJoinToken(ctx, string(params.Data.Payload))   // 模式A
} else {
    attestResult, err = s.attestChallengeResponse(ctx, stream, params)        // 模式B
}

模式 A — join_token(attestJoinToken,service.go:654):一次性预共享令牌,不需要插件,查库校验后用后即删防重放,SPIFFE ID 由 token 派生:

go
// pkg/server/api/agent/v1/service.go:657
joinToken, err := s.ds.FetchJoinToken(ctx, token)
...
err = s.ds.DeleteJoinToken(ctx, token)          // 用后即删
...
agentID, err := joinTokenID(s.td, token)         // spiffe://<td>/spire/agent/join_token/<uuid>

模式 B — 挑战应答类(attestChallengeResponse,service.go:683):适用于云平台 IID(aws_iid/gcp_iit/azure_msi)、x509popk8s_psattpm_devid 等。通过 catalog 找到同名 Server 插件,插件验证平台签名/证书并可发起挑战,SPIFFE ID 由平台元数据生成(如 spiffe://<td>/spire/agent/aws_iid/<account>/<region>/<instance-id>),并附带节点 selectors:

go
// pkg/server/api/agent/v1/service.go:687
nodeAttestor, ok := s.cat.GetNodeAttestorNamed(attestorType)
result, err := nodeAttestor.Attest(ctx, params.Data.Payload, func(ctx, challenge) ([]byte, error) {
    agentStream.Send(&agentv1.AttestAgentResponse{Step: &..._Challenge{Challenge: challenge}})
    req, err := agentStream.Recv()
    return req.GetChallengeResponse(), nil       // 把挑战转发给 agent,收回应答
})

拿到 attestResult 后的通用后处理:封禁检查 → 签发 → 存节点 selectors → 落库:

go
// pkg/server/api/agent/v1/service.go:361
attestedNode, err := s.ds.FetchAttestedNode(ctx, agentID.String())
if attestedNode != nil && nodeutil.IsAgentBanned(attestedNode) {
    return ... "agent is banned"
}
svid, err := s.signSvid(ctx, agentID, params.Params.Csr, log)              // 签发(→ 链路三)
err = s.ds.SetNodeSelectors(ctx, agentID.String(), selector.Dedupe(attestResult.Selectors))

首次写入 attested_nodes 表,复用则更新:

go
// pkg/server/api/agent/v1/service.go:383
if attestedNode == nil {
    node := &common.AttestedNode{
        AttestationDataType: params.Data.Type,
        SpiffeId:            agentID.String(),
        CertNotAfter:        svid[0].NotAfter.Unix(),
        CertSerialNumber:    svid[0].SerialNumber.String(),
        CanReattest:         attestResult.CanReattest,
    }
    s.ds.CreateAttestedNode(ctx, node)
} else {
    ... s.ds.UpdateAttestedNode(ctx, node, api.UpdateAttestedNodeCertificateMask)
}

实际签名落点 signSvid(service.go:627)——SPIFFE ID 用 agentID(Server 决定),公钥取自 CSR:

go
// pkg/server/api/agent/v1/service.go:627
parsedCsr, err := x509.ParseCertificateRequest(csr)
x509Svid, err := s.ca.SignAgentX509SVID(ctx, ca.AgentX509SVIDParams{
    SPIFFEID:  agentID,
    PublicKey: parsedCsr.PublicKey,
})

2.3 Agent SVID 的轮换(闭环)

到期前由 pkg/agent/svid/rotator.go 轮换。根据节点是否 Reattestable 二选一:

go
// pkg/agent/svid/rotator.go:217
if r.c.RotationStrategy.ShouldRotateX509(r.clk.Now(), state.SVID[0]) || r.IsTainted() {
    if state.Reattestable {
        err = r.reattest(ctx)      // 重新走完整 AttestAgent(rotator.go:233)
    } else {
        err = r.rotateSVID(ctx)    // 轻量续期 RenewAgent(rotator.go:288)
    }
}

3. 链路二:工作负载认证 → Workload 领取 SVID

Workload 通过 Workload API(Unix domain socket 上的 gRPC)向本机 Agent 索要身份。整条链路的精髓:Agent 用调用进程的内核属性(PID→selectors)判定"你是谁",再把匹配到的、早已预签好的 SVID 递给你

3.1 Workload API 入口

Handler 实现 SPIFFE Workload API(pkg/agent/endpoints/workload/handler.go:69),四个方法:

go
// pkg/agent/endpoints/workload/handler.go:52
MethodFetchX509SVID    = "/SpiffeWorkloadAPI/FetchX509SVID"
MethodFetchJWTSVID     = "/SpiffeWorkloadAPI/FetchJWTSVID"
MethodFetchX509Bundles = "/SpiffeWorkloadAPI/FetchX509Bundles"
MethodFetchJWTBundles  = "/SpiffeWorkloadAPI/FetchJWTBundles"

FetchX509SVID(handler.go:252)是 server-streaming(证书轮换时持续推新):

go
// pkg/agent/endpoints/workload/handler.go:252
func (h *Handler) FetchX509SVID(_ *workload.X509SVIDRequest, stream ...) error {
    selectors, err := h.c.Attestor.Attest(ctx)                          // ① workload attestation
    subscriber, err := h.c.Manager.SubscribeToCacheChanges(ctx, selectors)  // ② 用 selectors 订阅缓存
    defer subscriber.Finish()
    for {
        select {
        case update := <-subscriber.Updates():
            h.sendX509SVIDResponse(update, stream, selectors, log, start) // ③ 推送(含轮换)
        case <-ctx.Done():
            return nil
        }
    }
}

3.2 对调用进程做 Workload Attestation(PID → selectors)

Handler.Attestor 的实现 PeerTrackerAttestor(pkg/agent/endpoints/peertracker.go:17)从 peertracker 拿到调用者 PID,attestation 后再校验进程仍存活(防 PID 复用攻击):

go
// pkg/agent/endpoints/peertracker.go:17
func (a PeerTrackerAttestor) Attest(ctx context.Context) ([]*common.Selector, error) {
    watcher, ok := peertracker.WatcherFromContext(ctx)
    selectors, err := a.Attestor.Attest(ctx, int(watcher.PID()))    // 真正的 attestor
    if err := watcher.IsAlive(); err != nil {                        // 确认原始调用者还活着
        return nil, status.Errorf(codes.Unauthenticated, "could not verify existence of the original caller: %v", err)
    }
    return selectors, nil
}

真正的 workload attestor(pkg/agent/attestor/workload/workload.go:112)并行跑所有插件(unix/docker/k8s/systemd/windows),汇总 selectors(如 unix:uid:1000k8s:ns:defaultdocker:label:...):

go
// pkg/agent/attestor/workload/workload.go:112
plugins := wla.c.Catalog.GetWorkloadAttestors()
for _, p := range plugins {
    wg.Go(func() {
        if selectors, err := attestFunc(p); err == nil { sChan <- selectors }
    })
}

3.3 selectors 匹配 entries,从缓存取 SVID

Agent 缓存 Manager 委托给 LRU 缓存:

go
// pkg/agent/manager/manager.go:288
func (m *manager) MatchingRegistrationEntries(selectors) []*common.RegistrationEntry {
    return m.cache.MatchingRegistrationEntries(selectors)   // selectors ⊆ entry.Selectors 的 entry
}

X509-SVID 已预先缓存好(见 3.4),composeX509SVIDResponse(handler.go:455)直接取出证书链 + 本地私钥 + bundle 打包:

go
// pkg/agent/endpoints/workload/handler.go:466
for _, identity := range update.Identities {
    keyData, err := x509.MarshalPKCS8PrivateKey(identity.PrivateKey)   // 私钥来自 agent 本地
    svid := &workload.X509SVID{
        SpiffeId:    identity.Entry.SpiffeId,
        X509Svid:    x509util.DERFromCertificates(identity.SVID),      // 缓存的证书链
        X509SvidKey: keyData,
        Bundle:      bundle,
    }
    resp.Svids = append(resp.Svids, svid)
}

X509 vs JWT 的本质差异:X509-SVID 可以预签+缓存(私钥在 agent 本地,证书是公开信息);JWT-SVID 无法本地签(JWT 私钥只在 Server),因此走 Manager.FetchJWTSVID(manager.go:309)缓存优先、未命中/将过期才回源现签:

go
// pkg/agent/manager/manager.go:322
if !bypassCache {
    cachedSVID, ok = m.cache.GetJWTSVID(spiffeID, audience)
    if ok && !m.c.RotationStrategy.JWTSVIDExpiresSoon(cachedSVID, now) {
        return cachedSVID, nil                                // 缓存命中
    }
}
newSVID, svidSPIFFEID, err := m.client.NewJWTSVID(ctx, entry.EntryId, audience, isCacheHit)  // 回源现签
m.cache.SetJWTSVID(svidSPIFFEID, audience, newSVID)           // 回填

IncludeJTI 的 entry 会 bypassCache,每次现签以保证 jti 唯一。

3.4 X509-SVID 的预签发与缓存(后台 sync 循环)

Agent 周期性 synchronize(pkg/agent/manager/sync.go:102):拉取本 agent 有权获得的 entries → 对过期/缺失/有变更的 SVID 批量续签:

go
// pkg/agent/manager/sync.go:173
staleEntries := c.GetStaleEntries()
for _, entry := range staleEntries {
    if len(csrs) >= sizeLimit { break }               // CSR 数量限流(退避)
    csrs = append(csrs, csrRequest{...})
}
update, err := m.fetchSVIDs(ctx, csrs)
c.UpdateSVIDs(update)                                  // 写回缓存

fetchSVIDs(sync.go:205)为每个 entry 本地生成密钥对+CSR(私钥永不出 agent),批量请求 Server:

go
// pkg/agent/manager/sync.go:243
privateKey, csrBytes, err := newCSR(spiffeID, m.c.WorkloadKeyType)
...
svidsOut, err := m.client.NewX509SVIDs(ctx, csrsIn)   // 批量请求(client.go:312 → BatchNewX509SVID)
...
byEntryID[entryID] = &cache.X509SVID{Chain: chain, PrivateKey: privateKey}  // 链来自 server,私钥来自本地

3.5 Server 侧:批量签发 workload SVID

BatchNewX509SVID(pkg/server/api/svid/v1/service.go:193)先授权校验(防越权:agent 只能取到它有权代理的 entry),再逐条签:

go
// pkg/server/api/svid/v1/service.go:210
entriesMap, err := s.findEntries(ctx, log, requestedEntries)   // LookupAuthorizedEntries
for _, svidParam := range req.Params {
    r := s.newX509SVID(ctx, svidParam, entriesMap)
    results = append(results, r)
}

newX509SVID(service.go:260)校验 CSR 签名,SPIFFE ID / TTL / DNSNames 全部来自 entry(而非 CSR):

go
// pkg/server/api/svid/v1/service.go:290
if err := csr.CheckSignature(); err != nil { ... }
spiffeID, err := api.TrustDomainMemberIDFromProto(ctx, s.td, entry.GetSpiffeId())   // 以 entry 为准
x509Svid, err := s.ca.SignWorkloadX509SVID(ctx, ca.WorkloadX509SVIDParams{
    SPIFFEID:  spiffeID,
    PublicKey: csr.PublicKey,
    DNSNames:  entry.GetDnsNames(),
    TTL:       time.Duration(entry.GetX509SvidTtl()) * time.Second,
})

4. 链路三:CA 实际签名("颁发"的落点)

4.1 ServerCA 接口与 CA 实现

接口定义(pkg/server/ca/ca.go:33),这是整个"颁发"的中枢:

go
// pkg/server/ca/ca.go:33
type ServerCA interface {
    SignDownstreamX509CA(ctx, params DownstreamX509CAParams) ([]*x509.Certificate, error)  // 给下游/nested server 签中间 CA
    SignServerX509SVID(ctx, params ServerX509SVIDParams) ([]*x509.Certificate, error)      // server 自身 SVID
    SignAgentX509SVID(ctx, params AgentX509SVIDParams) ([]*x509.Certificate, error)        // agent SVID
    SignWorkloadX509SVID(ctx, params WorkloadX509SVIDParams) ([]*x509.Certificate, error)  // workload X509-SVID
    SignWorkloadJWTSVID(ctx, params WorkloadJWTSVIDParams) (string, error)                 // workload JWT-SVID
    ...
}

CA 结构(ca.go:167)用读写锁保护当前签名材料:x509CA(签名器+证书)、x509CAChain(下发链)、jwtKey(签名器+kid+过期时间)。

4.2 X509-SVID 签名主干

以 Agent 为例(Workload 相同,仅模板不同):

go
// pkg/server/ca/ca.go:308
func (ca *CA) SignAgentX509SVID(ctx, params AgentX509SVIDParams) ([]*x509.Certificate, error) {
    x509CA, caChain, err := ca.getX509CA()                    // 取当前签名 CA(可能带上游链)
    template, err := ca.c.CredBuilder.BuildAgentX509SVIDTemplate(ctx, credtemplate.AgentX509SVIDParams{
        ParentChain: caChain, PublicKey: params.PublicKey, SPIFFEID: params.SPIFFEID,
    })
    svidChain, err := ca.signX509SVID(x509CA, template)       // 实际签名
    if err := ca.c.CredValidator.ValidateX509SVID(svidChain[0], params.SPIFFEID); err != nil { ... }  // 签后校验
    return svidChain, nil
}

签名主干 signX509SVID(ca.go:437)——用 CA 证书 + CA 私钥签模板,并把上游链拼到叶子后面:

go
// pkg/server/ca/ca.go:437
func (ca *CA) signX509SVID(x509CA *X509CA, template *x509.Certificate) ([]*x509.Certificate, error) {
    x509SVID, err := x509util.CreateCertificate(template, x509CA.Certificate, template.PublicKey, x509CA.Signer)
    return makeCertChain(x509CA, x509SVID), nil               // [leaf, upstreamChain...]
}

4.3 证书模板:URI SAN / 密钥用途 / 有效期

模板在 pkg/server/credtemplate/builder.go,三层结构 buildBaseTemplate(:471)→ buildX509SVIDTemplate(:438)→ BuildXxxX509SVIDTemplate

SPIFFE ID 写入 URI SAN(唯一身份锚点):

go
// pkg/server/credtemplate/builder.go:489
return &x509.Certificate{
    SerialNumber:          serialNumber,
    URIs:                  []*url.URL{spiffeID.URL()},   // ← SPIFFE ID 作为 URI SAN
    SubjectKeyId:          subjectKeyID,
    AuthorityKeyId:        authorityKeyID,               // 指向签发 CA
    BasicConstraintsValid: true,
    PublicKey:             publicKey,
}, nil

密钥用途(已核对 builder.go:460)——一张证书兼作 mTLS 两端:

go
// pkg/server/credtemplate/builder.go:460
tmpl.KeyUsage = x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature
tmpl.ExtKeyUsage = []x509.ExtKeyUsage{ x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth }

有效期封顶(computeCappedLifetime,builder.go:541)——notBefore = now - 10s(时钟偏移缓冲),notAfter 被父 CA 到期时间封顶,SVID 不能活得比签它的 CA 更久:

go
// pkg/server/credtemplate/builder.go:541
notBefore = now.Add(-NotBeforeCushion)      // 10s 缓冲
notAfter = now.Add(ttl)
if !expirationCap.IsZero() && notAfter.After(expirationCap) {
    notAfter = expirationCap                 // 封顶
}

4.4 JWT-SVID 签名:claims / kid / 有效期

SignWorkloadJWTSVID(ca.go:365)构建 claims 再签,有效期被 JWT 签名密钥寿命封顶:

go
// pkg/server/credtemplate/builder.go:347  (BuildWorkloadJWTSVIDClaims)
attributes := credentialcomposer.JWTSVIDAttributes{
    Claims: map[string]any{
        "sub": params.SPIFFEID.String(),        // subject = SPIFFE ID
        "exp": jwt.NewNumericDate(expiresAt),
        "aud": params.Audience,                 // 受众
        "iat": jwt.NewNumericDate(now),
    },
}
if params.IncludeJTI { attributes.Claims["jti"] = ... }   // 可选唯一 ID(可审计)

实际签名 signJWTSVID(ca.go:446)——kid 取自 jwtKey.Kid(供验证方在 bundle 里选公钥),私钥用 cryptosigner.Opaque 包裹不导出:

go
// pkg/server/ca/ca.go:452
jwtSigner, err := jose.NewSigner(jose.SigningKey{
    Algorithm: alg,
    Key: jose.JSONWebKey{
        Key:   cryptosigner.Opaque(jwtKey.Signer),   // 私钥留在 KeyManager 后端(可 HSM/KMS)
        KeyID: jwtKey.Kid,                            // ← kid
    },
}, new(jose.SignerOptions).WithType("JWT"))
signedToken, err := jwt.Signed(jwtSigner).Claims(claims).Serialize()

4.5 CA 密钥来源、自签 vs 上游链、prepare/activate/rotate

CA 签名材料由 Server CA 生命周期 Manager(pkg/server/ca/manager/manager.go)管理,双槽(current/next)+ journal 持久化。密钥来自 KeyManager 插件,自签或上游二选一:

go
// pkg/server/ca/manager/manager.go:279  (PrepareX509CA)
km := m.c.Catalog.GetKeyManager()
signer, err := km.GenerateKey(ctx, slot.KmKeyID(), m.c.X509CAKeyType)   // 私钥由 KeyManager 托管
if m.upstreamClient != nil {
    x509CA, err = m.upstreamSignX509CA(ctx, signer)    // 有 UpstreamAuthority:做中间 CA
} else {
    x509CA, err = m.selfSignX509CA(ctx, signer)        // 无上游:自签根 CA
}
m.journal.AppendX509CA(ctx, slot.id, slot.issuedAt, slot.x509CA)   // 写 journal(供多 server 共享/恢复)
  • 上游模式保存完整 UpstreamChain(manager.go:992),正是 makeCertChain 拼到叶子后面的链——也是 Nested SPIRE 的关键(详见 高可用)。
  • 三阶段:PrepareX509CA(生成+签,状态 PREPARED)→ ActivateX509CAactivateX509CA(manager.go:743m.c.CA.SetX509CA(...) 把 CA 注入签名器)→ RotateX509CA(current/next 交换槽)。
  • JWT 密钥同理:PrepareJWTKey(notAfter = now + caTTL)→ activateJWTKey(SetJWTKey)。这个 NotAfter 就是 4.4 里 JWT-SVID 有效期的封顶值。

5. 端到端时序图


6. 关键函数速查表

环节函数位置
链路一 Agent 发起 attestationattestor.Attest / newSVIDpkg/agent/attestor/node/node.go:65 / :218
Server 处理 attestationService.AttestAgentpkg/server/api/agent/v1/service.go:294
join_token 模式attestJoinTokenservice.go:654
挑战应答/云平台模式attestChallengeResponseservice.go:683
签 Agent SVIDsignSvidca.SignAgentX509SVIDservice.go:627 / ca.go:308
链路二 Workload API 入口FetchX509SVID / FetchJWTSVIDpkg/agent/endpoints/workload/handler.go:252 / :95
PID→selectorsPeerTrackerAttestor.Attestpkg/agent/endpoints/peertracker.go:17
并行跑插件attestor.attestpkg/agent/attestor/workload/workload.go:112
selectors→entriesMatchingRegistrationEntriespkg/agent/manager/manager.go:288
组装 X509 响应composeX509SVIDResponsehandler.go:455
预签+缓存synchronize/updateSVIDs/fetchSVIDspkg/agent/manager/sync.go:102/:169/:205
Server 批量签BatchNewX509SVID/newX509SVIDpkg/server/api/svid/v1/service.go:193 / :260
Server JWTNewJWTSVID/mintJWTSVIDservice.go:377 / :332
链路三 接口ServerCApkg/server/ca/ca.go:33
X509 签名主干signX509SVIDpkg/server/ca/ca.go:437
JWT 签名主干signJWTSVIDpkg/server/ca/ca.go:446
证书模板/URI SANbuildBaseTemplatepkg/server/credtemplate/builder.go:471
密钥用途/有效期buildX509SVIDTemplate/computeCappedLifetimebuilder.go:438 / :541
CA 密钥来源+自签/上游PrepareX509CApkg/server/ca/manager/manager.go:258
注入签名器activateX509CASetX509CAmanager.go:743 / ca.go:204

7. 贯穿全程的安全设计要点

  1. 主体不能自证身份:agent/workload 私钥本地生成,但 CSR 里不含 URI SAN(node.go:229sync.go:243),SPIFFE ID 完全由 Server 依据 attestation 结果 / registration entry 决定(agent/v1/service.go:339svid/v1/service.go:290)。
  2. JWT 私钥只在 Server:JWT-SVID 无法本地签,必须回源;cryptosigner.Opaque(ca.go:452)确保私钥留在 KeyManager 后端(可为 HSM/KMS)。
  3. 防越权:workload SVID 批量签发前 LookupAuthorizedEntries(svid/v1/service.go)确保 agent 只能取到它有权代理的 entry。
  4. 防 PID 复用:workload attestation 后 watcher.IsAlive()(peertracker.go)二次确认调用者存活。
  5. 有效期封顶链:SVID 寿命被签发 CA 封顶,CA 被上游封顶(computeCappedLifetime),形成自上而下的失效传播。
  6. 签后校验:每次签名后 CredValidator.ValidateX509SVID/ValidateWorkloadJWTSVID,防止 credential composer 插件把模板改坏。

8. SVID 的构成与配置(字段 + 配置项详解)

8.1 X509-SVID 的构成

一张 X509-SVID 就是一张 X.509 叶子证书 +(本地)私钥 + 用于验证的 trust bundle。证书字段(全部由 credtemplate/builder.go 构造,已核对):

字段出处
URI SAN唯一身份 = SPIFFE ID(恰好一个 URI)builder.go:489
Subject默认 C=US, O=SPIRE;若 entry 配了 -dns,则 CN = DNSNames[0]builder.go:61:311
DNS SANentry 的 -dns(可多个)builder.go:312
KeyUsage(critical)digitalSignature + keyEncipherment + keyAgreementbuilder.go:460
ExtKeyUsageserverAuth + clientAuth(∴一张证书可同时作 mTLS 两端)builder.go:463
BasicConstraintscA = false(叶子证书,不能再签)规范 §4.1
SerialNumber / SKI / AKI随机序列号 / 公钥摘要 / 指向签发 CAbuilder.go:489
NotBefore / NotAfternow − 10s(时钟缓冲) / now + TTL,且被签发 CA 到期时间封顶builder.go:541

对比:X509 CA / 中间 CA 证书是 cA=true + keyCertSign(builder.go:432),默认 Subject C=US, O=SPIFFE(builder.go:52)。

8.2 JWT-SVID 的构成

一个签名 JWT(紧凑序列化,header.payload.signature):

部分字段出处
Headeralg(从签名密钥推导)、kid(供验证方在 bundle 里选公钥)、typ=JWTca.go:452
Claimssub = SPIFFE ID;aud = 受众(必填);expiat;可选 jti(唯一 ID,-includeJTI 或审计需要);可选 iss(= server 配置的 jwt_issuer)builder.go:349-364

验证方用 bundle 里 kid 对应的公钥验签,再校验 aud/exp/sub

8.3 WIT-SVID(较新,Workload Identity Token)

也是 JWT,但语义是"持有者令牌":claims 为 sub/exp/iat + cnf(confirmation,绑定持有者公钥) + 可选 iss(wit_issuer)——builder.go:407-415。用于需要 proof-of-possession 的场景。默认不启用。

8.4 配置项:三个层级

SVID 的"长什么样、活多久"由三个层级的配置共同决定:

① Server 全局(server.confserver {} 块) —— 决定 CA 与默认 TTL:

配置项默认作用
ca_ttl24hCA/签名密钥寿命,封顶所有下游 SVID 的有效期
ca_key_typeec-p256CA 密钥类型(X509 与 JWT 共用)
ca_subject { ... }C=US,O=SPIFFECA 证书 Subject
default_x509_svid_ttl1hX509-SVID 默认 TTL
default_jwt_svid_ttl5mJWT-SVID 默认 TTL
jwt_issuerJWT-SVID 的 iss claim

出处:conf/server/server_full.conf:19-203。TTL 关系:SVID TTL ≤ CA 剩余寿命 ≤ 上游 CA 寿命

② 注册条目(spire-server entry create,存 DataStore) —— 决定单个身份的形态,覆盖全局默认:

flag作用
-spiffeID该条目签发的 SVID 身份(URI SAN)
-parentID父(agent SPIFFE ID 或 node alias),决定哪个 agent 能领
-selector(可重复)工作负载匹配条件(如 unix:uid:1000k8s:ns:default)
-x509SVIDTTL / -jwtSVIDTTL(秒)覆盖全局默认 TTL
-dns(可重复)X509-SVID 的 DNS SAN(第一个也作 CN)
-federatesWith(可重复)该 SVID 需联邦哪些外域(下发时带上对应 bundle)
-hint同一 SPIFFE ID 多条目时消歧
-downstream标记为下游 SPIRE server(可调 NewDownstreamX509CA 领中间 CA,见 高可用 §4)
-admin授予该 SPIFFE ID 访问 Server 管理 API
-storeSVID结果 SVID 通过 SVIDStore 插件存储(而非走 Workload API)
-node该条目应用于节点(node alias)而非工作负载

出处:cmd/spire-server/cli/entry/create.go:100-113

③ Agent 全局(agent.conf) —— 决定本地私钥类型与轮换:workload_x509_svid_key_type(工作负载私钥类型)、availability_target(轮换提前量,见 高可用 §6.3)等。私钥始终由 agent 本地生成、永不出机。

8.5 一句话

"构成" = 由 Server 的证书/JWT 模板(credtemplate)按 SPIFFE 规范固定填充(URI SAN、KeyUsage、claims…);"配置" = 全局默认(server.conf 的 CA/TTL)每条注册条目(entry 的 TTL/DNS/federatesWith…) 覆盖,私钥类型再由 agent.conf 定——三层叠加决定每张 SVID 的最终形态与寿命。

内容基于 SPIFFE/SPIRE 官方开源资料整理与翻译,仅供学习交流;商标与版权归各自所有者。SPIFFE 与 SPIRE 是 CNCF 项目。