Skip to content

什么是 SVID —— 用一套三节点实战讲清"形式的决定因素"与"实际的决定因素"

本文不是复述规范,而是一套真实跑起来的 SPIRE 部署得出的结论。三节点全在 homelab LAN:

  • spire-server(VM 192.168.71.159,自签 CA,trust domain spire.qstarstar.com)
  • spire-agent(VM 192.168.71.126,node attestation = join_token,workload attestation = unix)
  • K3S agent(rancher 192.168.71.3 上的 DaemonSet,node attestation = k8s_psat,workload attestation = k8s)

所有证书/令牌都是这套环境真实签出来的,原件在 captured/。代码级细节回指 本站《SVID 颁发流程》(标注为 [02 §x])与源码 spire-src(路径:行号)。


0. 一句话:SVID 是什么

SVID = SPIFFE Verifiable Identity Document,是一个工作负载"身份"的可验证物理载体。它承载一个 SPIFFE ID:

spiffe://<trust-domain>/<path>

在本 lab 里,我们真的让两个不同平台的工作负载各领到一张 SVID:

工作负载平台领到的 SPIFFE ID载体
web(uid 1001 的进程)虚拟机spiffe://spire.qstarstar.com/vm/webX.509 证书
demo Pod(SA=svc)K3Sspiffe://spire.qstarstar.com/ns/demo/sa/svcX.509 证书

一张 X509-SVID = 一张 X.509 叶子证书 + 本地私钥 + 验证用的 trust bundle。下面是 VM 上那张的真容(captured/vm-workload-svid.pem 解出来的):

Issuer : C=CN, O=qstarstar-homelab, CN=spire.qstarstar.com   ← 签发它的自签 CA
Subject: C=US, O=SPIRE, CN=web.vm.qstarstar.lab
Validity: Not Before Jul 9 15:34:01 2026 / Not After 16:34:11 2026   ← ~1h
X509v3 Key Usage (critical): Digital Signature, Key Encipherment, Key Agreement
X509v3 Extended Key Usage : TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints (critical): CA:FALSE
X509v3 Subject Alternative Name:
    DNS:web.vm.qstarstar.lab,  URI:spiffe://spire.qstarstar.com/vm/web   ← 身份就藏在这个 URI SAN

身份 = 那个唯一的 URI SAN。证书别的字段都是"外壳",URI 才是"名字"。[02 §0]

SVID 有三种形态:X509-SVID(mTLS)、JWT-SVID(OIDC/无 mTLS 通道)、WIT-SVID(较新,持有者令牌,默认不启用)。本 lab 主要用 X509,并抓了一张 JWT 做对比(见 §1.2)。


1. 形式的决定因素:一张 SVID"长什么样、活多久、叫什么"由什么规定

"形式"= 在纸面上就能确定的东西:规范 + 类型 + 三层配置。它们决定 SVID 的结构与默认寿命,但不决定某个具体进程能不能拿到(那是 §2)。

1.1 因素①:SPIFFE 规范 —— 硬约束(不可配)

X509-SVID 的这些字段是规范钉死的,SPIRE 的证书模板照抄(spire-src/pkg/server/credtemplate/builder.go,[02 §4.3/§8.1]):

字段规范要求本 lab 实证
URI SAN有且仅有一个,即 SPIFFE ID✔ 两张 SVID 各恰好一个 URI
BasicConstraints叶子 CA:FALSE✔ 都是 CA:FALSE
KeyUsage(critical)digitalSignatureDigitalSignature, KeyEncipherment, KeyAgreement
ExtKeyUsageserverAuth+clientAuth✔ 两张都有 → 一张证书兼作 mTLS 两端

关键观察:VM 那张(/vm/web)和 K3S 那张(/ns/demo/sa/svc)—— 这些字段逐一相同。形式由规范统一规定,与工作负载在哪、是谁无关

1.2 因素②:SVID 类型(X.509 vs JWT vs WIT)

同一个身份 /vm/web,换个"取法"就是另一种形态。我们对同一进程 fetch jwt 拿到一张 JWT-SVID(captured/vm-jwt-svid.jwt):

header : {"alg":"ES256","kid":"iR9QHBkBgs5MSeOo5Q5UXA4rPmgjQlad","typ":"JWT"}
payload: {"aud":["nacos"], "sub":"spiffe://spire.qstarstar.com/vm/web", "iat":..., "exp":...}
  • sub = 同一个 SPIFFE ID;aud = 受众(必填);kid = 验证方在 bundle 里选公钥用。[02 §8.2]
  • 本质差异:X509-SVID 私钥在本地,可预签+缓存;JWT-SVID 私钥只在 Server(KeyManager 后端),必须回源现签。[02 §3.3]

所以"取 X.509 还是 JWT"是形式的决定因素——你请求哪种,就得到哪种载体。

1.3 因素③:三层配置(全局 → 条目 → agent)

SVID 的"寿命、名字、DNS…"由三层配置叠加决定,下层覆盖上层[02 §8.4]。本 lab 的真实取值:

📎 每个配置项影响什么、怎么亲手做正向/负向/AB 实验,见 EXPERIMENTS.md(含真实 before/after 输出:ca_ttl 封顶、-dns 塑形、-selector AND、unix:sha256 二进制指纹、EC⇄RSA、jwt_issuer 等)。

① Server 全局(server/server.conf)—— 定 CA 与默认,封顶所有下游:

配置本 lab 值作用证书里的体现
trust_domainspire.qstarstar.com信任域所有 SPIFFE ID 的前缀
ca_ttl24hCA 寿命,封顶 SVIDCA 证书 Not After = 签发+24h
ca_subjectC=CN,O=qstarstar-homelab,CN=spire.qstarstar.comCA 主体每张 SVID 的 Issuer
default_x509_svid_ttl1hX509 默认 TTL见下(被 entry 覆盖为 3600s,恰好也 1h)

寿命链:SVID TTL ≤ CA 剩余寿命 ≤ 上游 CA。本 lab 自签无上游,故 SVID 最长活到 CA 到期(captured/trust-bundle.pemNot After)。

② 注册条目 entry(server/entries.sh)—— 定单个身份的形态,覆盖全局:

bash
# VM 身份:带一个 DNS SAN(于是证书 CN = DNS[0] = web.vm.qstarstar.lab)
entry create -spiffeID .../vm/web        -selector unix:uid:1001 -dns web.vm.qstarstar.lab -x509SVIDTTL 3600
# K3S 身份:没配 -dns,于是证书没有 CN、没有 DNS SAN
entry create -spiffeID .../ns/demo/sa/svc -selector k8s:ns:demo -selector k8s:sa:svc     -x509SVIDTTL 3600

对上真实证书,一眼看出 entry 的 -dns 如何塑形:

VM /vm/web(配了 -dns)K3S /ns/demo/sa/svc(没配)
SubjectC=US, O=SPIRE, **CN=web.vm.qstarstar.lab**C=US, O=SPIRE(无 CN)
DNS SANDNS:web.vm.qstarstar.lab
URI SANURI:spiffe://.../vm/webURI:spiffe://.../ns/demo/sa/svc

-spiffeID-dns-x509SVIDTTL-federatesWith 这些 entry 字段,就是"这一个身份长什么样"的直接决定因素。[02 §8.4 ②]

③ Agent 全局(agent-vm/agent.conf / K3S configmap)—— 定私钥类型与轮换。私钥始终由 agent 本地生成、永不出机(CSR 里甚至不含 SPIFFE ID)[02 §7.1]。

1.4 小结(形式)

形式 = SPIFFE 规范(结构硬约束)+ 你请求的 SVID 类型 + 三层配置(server.conf 的 CA/TTL、entry 的 ID/DNS/TTL、agent.conf 的私钥类型)。 它们决定 SVID 的骨架与默认寿命。证明:两张平台迥异的 SVID,形式字段完全一致,只有"名字"(URI)和是否带 DNS 不同——而这些差异也全都能在上面三层配置里找到出处。


2. 实际的决定因素:一个具体进程到底能不能拿到、拿到哪张 SVID

这才是 SPIRE 的灵魂,一句话:主体不能自证身份[02 §7.1]。工作负载说自己是谁没用,它拿到哪张 SVID,由运行时"能被 attestation 证明的事实"决定。分两级。

2.1 第一级:Node Attestation —— 决定 agent 自己是谁

agent 上线时先证明"我是哪台节点"。用什么方法认证,直接决定 agent 的 SPIFFE ID 结构。本 lab 两个 agent 的真实身份(spire-server agent list 抓的):

agent认证方法真实拿到的 SPIFFE ID可重认证
VMjoin_tokenspiffe://…/spire/agent/**join_token**/049212fc-a3e7-4b74-9baf-ac826c08427bfalse
K3Sk8s_psatspiffe://…/spire/agent/**k8s_psat**/rancher-k3s/489360d9-7906-4b47-8919-9406c74069d3true

看清楚:同一台 server、同一个 trust domain,两个 agent 的身份结构完全不同——因为一个的身份从"一次性令牌"派生,另一个从"K8S 投影 SA 令牌经 TokenReview 校验后的节点 UID"派生。[02 §2.2] 连 reattestable 都不同(join_token 用后即删不可重认证;k8s_psat 可)——全是 attestation 方法的实际产物,没有一处是工作负载自己声称的。

node attestation 还顺带决定:该 agent 能当哪些 entry 的 parentID(即"哪个 agent 有资格代领哪些身份")。我们建 entry 时,-parentID 填的正是上面这串认证后才知道的 ID。

2.2 第二级:Workload Attestation —— 决定进程匹配到哪条 entry

工作负载通过 Workload API(Unix socket)向本机 agent 要 SVID。agent 不问它是谁,而是扒它的内核事实:[02 §3.2]

  • VM(unix attestor):调用进程的 uid/gid/path/sha256
  • K3S(k8s attestor):经 kubelet 把 PID→容器→Pod,得 k8s:ns / k8s:sa / k8s:pod-name …

这些 selector 去和 entry 匹配(规则:entry.Selectors ⊆ 进程实测 selectors,[02 §3.3]),命中才发对应 SVID。

2.3 实证一:VM 上"同配置、换 uid,结果就变"

entry 要求 unix:uid:1001。在同一台 VM、同一个 agent、同一份配置下,只把运行进程的 uid 换掉:

# 以 uid 1001(webapp)取:
$ sudo -u '#1001' spire-agent api fetch x509 ...
  SPIFFE ID: spiffe://spire.qstarstar.com/vm/web      ✔ 拿到

# 以 uid 1002(other)取:
$ sudo -u '#1002' spire-agent api fetch x509 ...
  [exit=1 —— 无匹配 entry,SPIRE 不发任何 SVID]       ✘ 什么都拿不到

决定"发不发、发哪张"的,不是配置(配置没变),而是运行时那个能被证明的 uid。这就是"实际的决定因素"。

2.4 实证二:K3S 上"同镜像、换 ServiceAccount,结果就变"

entry 要求 k8s:ns:demo k8s:sa:svc。两个 Pod 同一个 alpine 镜像、同一个节点、同一个 agent,只是 SA 不同:

# demo Pod(serviceAccountName: svc):
$ kubectl -n demo exec deploy/demo -- spire-agent api fetch x509 ...
  SPIFFE ID: spiffe://spire.qstarstar.com/ns/demo/sa/svc   ✔ 拿到

# demo-other Pod(serviceAccountName: default):
$ kubectl -n demo exec deploy/demo-other -- spire-agent api fetch x509 ...
  command terminated with exit code 1                       ✘ 缺 sa:svc,拿不到

镜像、命令、挂载全一样,唯一区别是 K8S 平台能证明的 ServiceAccount。身份由平台事实决定,不由容器自己决定。

2.5 还有两个"实际"约束

  • 授权边界(防越权):server 批量签发前 LookupAuthorizedEntries,agent 只能代领挂在它 parentID 名下的 entry——所以 K3S agent 拿不到 VM 的 /vm/web,反之亦然。[02 §3.5、§7.3]
  • 谁签的:SVID 的可验证性来自签它的那台 server / 那个 trust domain / 当前 CA 槽。本 lab 两张 workload SVID 的 Issuer 都是同一个自签 CA(captured/trust-bundle.pem),所以能被同一个 bundle 验证。

2.6 小结(实际)

实际 = 运行时能被 attestation 证明 的事实(在哪台机器 / 是什么进程)+ 与注册条目的匹配 + 授权边界 + 由谁签发。 它决定"这个具体进程能不能拿到、拿到哪张"。工作负载无法自选身份——uidk8s sa 变一下,结果立刻变。


3. 形式 vs 实际:一张表看穿

维度形式的决定因素实际的决定因素
回答的问题SVID 长什么样、活多久、叫什么(纸面)这个进程能不能拿到、拿到哪张(运行时)
谁说了算SPIFFE 规范 + SVID 类型 + 三层配置Node/Workload attestation 的实测结果 + entry 匹配 + 授权
输入server.conf、entry、agent.conf机器身份(join_token/k8s_psat…)、进程事实(uid/k8s ns·sa…)
本 lab 铁证两张 SVID 的 CA:FALSE/KeyUsage/EKU/单 URI 完全一致换 uid、换 sa → 拿到 vs 拿不到;两 agent 身份结构不同
能否被主体伪造否(模板由 server 填)否(主体不能自证身份)

贯穿全局的一句话:

SVID 的形式由"规范 + 配置"给定;但一个工作负载实际拿到哪张 SVID,取决于平台能替它证明什么,而不是它自己声称什么。 私钥本地生成、CSR 里不含身份(spire-src node.go:229sync.go:243),身份完全由 server 依据 attestation 结果 + registration entry 决定 —— 这就是"主体不能自证身份"。

killer demo 一句复现:同一份 entry、同一台 agent,把进程的 uid(或 K8S 的 ServiceAccount)换一下,拿到的 SVID 就从"有"变"无"。 形式没动,实际全变。


4. 附录

4.1 三节点与关键产物

角色位置认证方式产物
spire-serverVM 192.168.71.159:8081自签 CA / join_token+k8s_psatcaptured/trust-bundle.pem(根 CA)
VM agentVM 192.168.71.126node join_token / wl unixcaptured/vm-workload-svid.pemvm-jwt-svid.jwt
K3S agentrancher 192.168.71.3(DaemonSet)node k8s_psat / wl k8scaptured/k3s-workload-svid.pem

4.2 亲手复现

bash
# VM 上:换 uid 看结果(实际决定因素)
ssh soc@192.168.71.126 'sudo bash /path/demos/fetch-vm.sh 1001'   # 拿到 /vm/web
ssh soc@192.168.71.126 'sudo bash /path/demos/fetch-vm.sh 1002'   # 无身份

# K3S 上:换 ServiceAccount 看结果
ssh soc@192.168.71.3  'sudo bash /path/demos/fetch-k3s.sh demo'        # 拿到 /ns/demo/sa/svc
ssh soc@192.168.71.3  'sudo bash /path/demos/fetch-k3s.sh demo-other'  # 无身份

# 解读任意一张 SVID 的"形式"字段
bash demos/inspect-svid.sh captured/vm-workload-svid.pem

部署见 README.md;清理 bash teardown.sh(删两台 VM + spire/demo 两个 ns)。

4.3 更深的代码级走读

本文实证与结论,逐条可回指本站 源码解读:

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