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/frontendSPIRE 颁发三种 SVID:
| 形态 | 载体 | 私钥位置 | 典型用途 | 默认 TTL |
|---|---|---|---|---|
| X509-SVID | X.509 证书 | Agent/Workload 本地 | mTLS 双向认证 | 1h |
| JWT-SVID | 签名 JWT | Server(KeyManager 后端) | 面向 OIDC/无 mTLS 通道的认证 | 5m |
| WIT-SVID | JWT(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-SVID | pkg/server/api/agent/v1/service.go:627 signSvid | ca.SignAgentX509SVID (ca.go:308) |
| Workload X509-SVID | pkg/server/api/svid/v1/service.go:260 newX509SVID | ca.SignWorkloadX509SVID (ca.go:335) |
| Workload JWT-SVID | pkg/server/api/svid/v1/service.go:332 mintJWTSVID | ca.SignWorkloadJWTSVID (ca.go:365) |
⚠️ 容易混淆的两个 "Manager",先分清:
| 组件 | 路径 | 职责 |
|---|---|---|
| Agent 缓存 Manager | pkg/agent/manager/manager.go | Agent 侧维护 registration entries + workload SVID 缓存,周期同步 |
| Server CA 生命周期 Manager | pkg/server/ca/manager/manager.go | Server 侧 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;没有才走节点认证:
// 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) // 领取新 SVIDnewSVID(node.go:218)组装请求。关键点:生成不带 URI SAN 的 CSR——SPIFFE ID 由 Server 决定:
// 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) // 顺便拉取信任域 bundlegetSVID(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 类型二选一:
// 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 派生:
// 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)、x509pop、k8s_psat、tpm_devid 等。通过 catalog 找到同名 Server 插件,插件验证平台签名/证书并可发起挑战,SPIFFE ID 由平台元数据生成(如 spiffe://<td>/spire/agent/aws_iid/<account>/<region>/<instance-id>),并附带节点 selectors:
// 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 → 落库:
// 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 表,复用则更新:
// 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:
// 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 二选一:
// 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),四个方法:
// 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(证书轮换时持续推新):
// 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 复用攻击):
// 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:1000、k8s:ns:default、docker:label:...):
// 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 缓存:
// 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 打包:
// 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)缓存优先、未命中/将过期才回源现签:
// 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 批量续签:
// 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:
// 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),再逐条签:
// 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):
// 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),这是整个"颁发"的中枢:
// 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 相同,仅模板不同):
// 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 私钥签模板,并把上游链拼到叶子后面:
// 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(唯一身份锚点):
// 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 两端:
// 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 更久:
// 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 签名密钥寿命封顶:
// 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 包裹不导出:
// 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 插件,自签或上游二选一:
// 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)→ActivateX509CA→activateX509CA(manager.go:743的m.c.CA.SetX509CA(...)把 CA 注入签名器)→RotateX509CA(current/next 交换槽)。 - JWT 密钥同理:
PrepareJWTKey(notAfter = now + caTTL)→activateJWTKey(SetJWTKey)。这个NotAfter就是 4.4 里 JWT-SVID 有效期的封顶值。
5. 端到端时序图
6. 关键函数速查表
| 环节 | 函数 | 位置 |
|---|---|---|
| 链路一 Agent 发起 attestation | attestor.Attest / newSVID | pkg/agent/attestor/node/node.go:65 / :218 |
| Server 处理 attestation | Service.AttestAgent | pkg/server/api/agent/v1/service.go:294 |
| join_token 模式 | attestJoinToken | service.go:654 |
| 挑战应答/云平台模式 | attestChallengeResponse | service.go:683 |
| 签 Agent SVID | signSvid → ca.SignAgentX509SVID | service.go:627 / ca.go:308 |
| 链路二 Workload API 入口 | FetchX509SVID / FetchJWTSVID | pkg/agent/endpoints/workload/handler.go:252 / :95 |
| PID→selectors | PeerTrackerAttestor.Attest | pkg/agent/endpoints/peertracker.go:17 |
| 并行跑插件 | attestor.attest | pkg/agent/attestor/workload/workload.go:112 |
| selectors→entries | MatchingRegistrationEntries | pkg/agent/manager/manager.go:288 |
| 组装 X509 响应 | composeX509SVIDResponse | handler.go:455 |
| 预签+缓存 | synchronize/updateSVIDs/fetchSVIDs | pkg/agent/manager/sync.go:102/:169/:205 |
| Server 批量签 | BatchNewX509SVID/newX509SVID | pkg/server/api/svid/v1/service.go:193 / :260 |
| Server JWT | NewJWTSVID/mintJWTSVID | service.go:377 / :332 |
| 链路三 接口 | ServerCA | pkg/server/ca/ca.go:33 |
| X509 签名主干 | signX509SVID | pkg/server/ca/ca.go:437 |
| JWT 签名主干 | signJWTSVID | pkg/server/ca/ca.go:446 |
| 证书模板/URI SAN | buildBaseTemplate | pkg/server/credtemplate/builder.go:471 |
| 密钥用途/有效期 | buildX509SVIDTemplate/computeCappedLifetime | builder.go:438 / :541 |
| CA 密钥来源+自签/上游 | PrepareX509CA | pkg/server/ca/manager/manager.go:258 |
| 注入签名器 | activateX509CA→SetX509CA | manager.go:743 / ca.go:204 |
7. 贯穿全程的安全设计要点
- 主体不能自证身份:agent/workload 私钥本地生成,但 CSR 里不含 URI SAN(
node.go:229、sync.go:243),SPIFFE ID 完全由 Server 依据 attestation 结果 / registration entry 决定(agent/v1/service.go:339、svid/v1/service.go:290)。 - JWT 私钥只在 Server:JWT-SVID 无法本地签,必须回源;
cryptosigner.Opaque(ca.go:452)确保私钥留在 KeyManager 后端(可为 HSM/KMS)。 - 防越权:workload SVID 批量签发前
LookupAuthorizedEntries(svid/v1/service.go)确保 agent 只能取到它有权代理的 entry。 - 防 PID 复用:workload attestation 后
watcher.IsAlive()(peertracker.go)二次确认调用者存活。 - 有效期封顶链:SVID 寿命被签发 CA 封顶,CA 被上游封顶(
computeCappedLifetime),形成自上而下的失效传播。 - 签后校验:每次签名后
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 SAN | entry 的 -dns(可多个) | builder.go:312 |
| KeyUsage(critical) | digitalSignature + keyEncipherment + keyAgreement | builder.go:460 |
| ExtKeyUsage | serverAuth + clientAuth(∴一张证书可同时作 mTLS 两端) | builder.go:463 |
| BasicConstraints | cA = false(叶子证书,不能再签) | 规范 §4.1 |
| SerialNumber / SKI / AKI | 随机序列号 / 公钥摘要 / 指向签发 CA | builder.go:489 |
| NotBefore / NotAfter | now − 10s(时钟缓冲) / now + TTL,且被签发 CA 到期时间封顶 | builder.go:541 |
对比:X509 CA / 中间 CA 证书是
cA=true+keyCertSign(builder.go:432),默认 SubjectC=US, O=SPIFFE(builder.go:52)。
8.2 JWT-SVID 的构成
一个签名 JWT(紧凑序列化,header.payload.signature):
| 部分 | 字段 | 出处 |
|---|---|---|
| Header | alg(从签名密钥推导)、kid(供验证方在 bundle 里选公钥)、typ=JWT | ca.go:452 |
| Claims | sub = SPIFFE ID;aud = 受众(必填);exp、iat;可选 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.conf 的 server {} 块) —— 决定 CA 与默认 TTL:
| 配置项 | 默认 | 作用 |
|---|---|---|
ca_ttl | 24h | CA/签名密钥寿命,封顶所有下游 SVID 的有效期 |
ca_key_type | ec-p256 | CA 密钥类型(X509 与 JWT 共用) |
ca_subject { ... } | C=US,O=SPIFFE | CA 证书 Subject |
default_x509_svid_ttl | 1h | X509-SVID 默认 TTL |
default_jwt_svid_ttl | 5m | JWT-SVID 默认 TTL |
jwt_issuer | 空 | JWT-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:1000、k8s: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 的最终形态与寿命。