什么是 SVID —— 用一套三节点实战讲清"形式的决定因素"与"实际的决定因素"
本文不是复述规范,而是一套真实跑起来的 SPIRE 部署得出的结论。三节点全在 homelab LAN:
- spire-server(VM
192.168.71.159,自签 CA,trust domainspire.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/web | X.509 证书 |
demo Pod(SA=svc) | K3S | spiffe://spire.qstarstar.com/ns/demo/sa/svc | X.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) | 含 digitalSignature | ✔ DigitalSignature, KeyEncipherment, KeyAgreement |
| ExtKeyUsage | serverAuth+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塑形、-selectorAND、unix:sha256二进制指纹、EC⇄RSA、jwt_issuer等)。
① Server 全局(server/server.conf)—— 定 CA 与默认,封顶所有下游:
| 配置 | 本 lab 值 | 作用 | 证书里的体现 |
|---|---|---|---|
trust_domain | spire.qstarstar.com | 信任域 | 所有 SPIFFE ID 的前缀 |
ca_ttl | 24h | CA 寿命,封顶 SVID | CA 证书 Not After = 签发+24h |
ca_subject | C=CN,O=qstarstar-homelab,CN=spire.qstarstar.com | CA 主体 | 每张 SVID 的 Issuer |
default_x509_svid_ttl | 1h | X509 默认 TTL | 见下(被 entry 覆盖为 3600s,恰好也 1h) |
寿命链:
SVID TTL ≤ CA 剩余寿命 ≤ 上游 CA。本 lab 自签无上游,故 SVID 最长活到 CA 到期(captured/trust-bundle.pem的Not After)。
② 注册条目 entry(server/entries.sh)—— 定单个身份的形态,覆盖全局:
# 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(没配) | |
|---|---|---|
| Subject | C=US, O=SPIRE, **CN=web.vm.qstarstar.lab** | C=US, O=SPIRE(无 CN) |
| DNS SAN | DNS:web.vm.qstarstar.lab | 无 |
| URI SAN | URI:spiffe://.../vm/web | URI: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 | 可重认证 |
|---|---|---|---|
| VM | join_token | spiffe://…/spire/agent/**join_token**/049212fc-a3e7-4b74-9baf-ac826c08427b | false |
| K3S | k8s_psat | spiffe://…/spire/agent/**k8s_psat**/rancher-k3s/489360d9-7906-4b47-8919-9406c74069d3 | true |
看清楚:同一台 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(
unixattestor):调用进程的uid/gid/path/sha256 - K3S(
k8sattestor):经 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 证明 的事实(在哪台机器 / 是什么进程)+ 与注册条目的匹配 + 授权边界 + 由谁签发。 它决定"这个具体进程能不能拿到、拿到哪张"。工作负载无法自选身份——
uid、k8s 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-srcnode.go:229、sync.go:243),身份完全由 server 依据 attestation 结果 + registration entry 决定 —— 这就是"主体不能自证身份"。
killer demo 一句复现:同一份 entry、同一台 agent,把进程的 uid(或 K8S 的 ServiceAccount)换一下,拿到的 SVID 就从"有"变"无"。 形式没动,实际全变。
4. 附录
4.1 三节点与关键产物
| 角色 | 位置 | 认证方式 | 产物 |
|---|---|---|---|
| spire-server | VM 192.168.71.159:8081 | 自签 CA / join_token+k8s_psat | captured/trust-bundle.pem(根 CA) |
| VM agent | VM 192.168.71.126 | node join_token / wl unix | captured/vm-workload-svid.pem、vm-jwt-svid.jwt |
| K3S agent | rancher 192.168.71.3(DaemonSet) | node k8s_psat / wl k8s | captured/k3s-workload-svid.pem |
4.2 亲手复现
# 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 更深的代码级走读
本文实证与结论,逐条可回指本站 源码解读:
02-SVID颁发流程.md—— 三条颁发链路代码级详解、字段与配置项。01-架构总览.md/03-多活与高可用.md