三节点部署与复现
把 实战板块 那套环境从零搭起来:一个 spire-server + 一个 VM agent(join_token / unix)+ 一个 K3S agent(k8s_psat / k8s)。改改 IP 和域名就能在你自己的环境复现。
前置:能到目标网络、SPIRE 二进制 = 官方
spire-1.15.1-linux-amd64-musl.tar.gz(解包到/opt/spire,软链到/usr/local/bin)。下文 IP/域名是作者 homelab 的值,请替换成你自己的。
1. spire-server
server.conf
自签 CA(无 UpstreamAuthority)、SQLite、同时挂 join_token 与 k8s_psat 两种节点认证。
server {
bind_address = "0.0.0.0"
bind_port = "8081"
trust_domain = "spire.qstarstar.com"
data_dir = "/opt/spire/data/server"
log_level = "INFO"
# —— CA 与默认 TTL(封顶所有 SVID)——
ca_ttl = "24h" # CA/签名密钥寿命,封顶所有下游 SVID
default_x509_svid_ttl = "1h" # X509-SVID 默认 TTL(entry 可覆盖)
default_jwt_svid_ttl = "5m" # JWT-SVID 默认 TTL
ca_subject { # 会出现在每张 SVID 的 issuer 里
country = ["CN"]
organization = ["qstarstar-homelab"]
common_name = "spire.qstarstar.com"
}
}
plugins {
DataStore "sql" {
plugin_data {
database_type = "sqlite3"
connection_string = "/opt/spire/data/server/datastore.sqlite3"
}
}
KeyManager "disk" { # CA 私钥落盘;生产可换 KMS/HSM
plugin_data { keys_path = "/opt/spire/data/server/keys.json" }
}
NodeAttestor "join_token" { plugin_data {} } # 给 VM agent 用
NodeAttestor "k8s_psat" { # 给 K3S agent 用
plugin_data {
clusters = {
"rancher-k3s" = {
service_account_allow_list = ["spire:spire-agent"]
kube_config_file = "/opt/spire/k3s.kubeconfig" # 用于 TokenReview + pod→node
}
}
}
}
}启动后导出信任根 bundle(agent 启动要用):
systemctl start spire-server
spire-server bundle show -format pem > trust-bundle.pem # 分发给两个 agent建注册条目(entries.sh)
-parentID 必须填 agent 认证后才知道的 SPIFFE ID(spire-server agent list 查)。
TD="spire.qstarstar.com"; SOCK=/tmp/spire-server/private/api.sock
S="spire-server entry create -socketPath $SOCK"
# VM 身份:由 unix:uid:1001 决定;带一个 DNS SAN(演示 CN=DNS[0])
$S -parentID "<VM-AGENT-ID>" \
-spiffeID "spiffe://$TD/vm/web" \
-selector "unix:uid:1001" \
-dns "web.vm.qstarstar.lab" \
-x509SVIDTTL 3600
# K3S 身份:由 k8s:ns:demo + k8s:sa:svc 共同决定(AND)
$S -parentID "<K3S-AGENT-ID>" \
-spiffeID "spiffe://$TD/ns/demo/sa/svc" \
-selector "k8s:ns:demo" \
-selector "k8s:sa:svc" \
-x509SVIDTTL 36002. VM agent(join_token / unix)
agent.conf
agent {
data_dir = "/opt/spire/data/agent"
server_address = "192.168.71.159"
server_port = "8081"
socket_path = "/run/spire/agent.sock" # Workload API 的 Unix socket
trust_domain = "spire.qstarstar.com"
trust_bundle_path = "/opt/spire/bootstrap-bundle.pem" # 上一步导出的 trust-bundle.pem
}
plugins {
NodeAttestor "join_token" { plugin_data {} }
KeyManager "disk" { plugin_data { directory = "/opt/spire/data/agent" } }
WorkloadAttestor "unix" {
plugin_data { discover_workload_path = true } # 额外产出 unix:path / unix:sha256(二进制指纹)
}
}上线
# server 上生成一次性 join token
spire-server token generate -spiffeID spiffe://spire.qstarstar.com/host
# agent 上用 token 启动(join_token 用后即删)
spire-agent run -config /opt/spire/agent.conf -joinToken <token>3. K3S agent(k8s_psat / k8s)
以 DaemonSet 每节点一个 agent 运行,hostPID + 共享 socket 目录让同节点 Pod 能取 SVID。核心 manifest(命名空间 / RBAC / ConfigMap / DaemonSet):
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: spire-agent, namespace: spire }
spec:
selector: { matchLabels: { app: spire-agent } }
template:
metadata: { labels: { app: spire-agent } }
spec:
hostPID: true
hostNetwork: true
serviceAccountName: spire-agent
containers:
- name: spire-agent
image: ghcr.io/spiffe/spire-agent:1.15.1
args: ["-config", "/run/spire/config/agent.conf"]
volumeMounts:
- { name: spire-config, mountPath: /run/spire/config, readOnly: true }
- { name: spire-bundle, mountPath: /run/spire/bundle, readOnly: true }
- { name: spire-token, mountPath: /var/run/secrets/tokens }
- { name: spire-agent-socket-dir, mountPath: /run/spire/sockets }
volumes:
- { name: spire-config, configMap: { name: spire-agent } }
- { name: spire-bundle, configMap: { name: spire-bundle } } # 由 server bundle 生成
- name: spire-agent-socket-dir
hostPath: { path: /run/spire/sockets, type: DirectoryOrCreate }
- name: spire-token
projected:
sources:
- serviceAccountToken:
path: spire-agent
expirationSeconds: 7200
audience: spire-server # 与 server k8s_psat 期望的受众一致ConfigMap 里的 agent.conf 关键差异(相对 VM):节点认证换成 k8s_psat,工作负载认证换成 k8s,KeyManager 用 memory。
demo 工作负载用同一个 alpine 镜像跑两个 Deployment,只有 ServiceAccount 不同,构成对照组:
# demo: serviceAccountName: svc -> 匹配 k8s:sa:svc -> 拿到 /ns/demo/sa/svc
# demo-other: serviceAccountName: default -> 缺 sa:svc -> 拿不到(对照组)4. 取证与对照(demos/)
# 解读任意一张 X509-SVID 的"形式"字段
inspect-svid () {
openssl x509 -in "$1" -noout -ext subjectAltName | grep -o 'URI:[^,]*' # 唯一身份
openssl x509 -in "$1" -noout -ext basicConstraints,keyUsage,extendedKeyUsage
openssl x509 -in "$1" -noout -issuer -dates
}
# VM:换 uid 看"实际决定因素"
sudo -u '#1001' spire-agent api fetch x509 -socketPath /run/spire/agent.sock # 拿到 /vm/web
sudo -u '#1002' spire-agent api fetch x509 -socketPath /run/spire/agent.sock # 无身份(exit 1)
# K3S:换 ServiceAccount 看结果
kubectl -n demo exec deploy/demo -- spire-agent api fetch x509 ... # 拿到 /ns/demo/sa/svc
kubectl -n demo exec deploy/demo-other -- spire-agent api fetch x509 ... # 无身份5. 清理
# 删 K3S 的 spire/demo 两个命名空间 + ESXi 上的两台 VM
bash teardown.sh附:真实抓取的样本
这套环境真实签出的样本(证书为公开材料,已解码展示;原始 JWT 令牌属 bearer 凭据,未公开发布):
VM 工作负载 X509-SVID(spiffe://spire.qstarstar.com/vm/web):
Issuer : C=CN, O=qstarstar-homelab, CN=spire.qstarstar.com ← 自签 CA
Subject: C=US, O=SPIRE, CN=web.vm.qstarstar.lab ← CN 来自 entry 的 -dns
Validity: ~1h(被 ca_ttl 24h 封顶)
X509v3 Key Usage (critical): Digital Signature, Key Encipherment, Key Agreement
X509v3 Extended Key Usage : TLS Web Server Auth, TLS Web Client Auth
X509v3 Basic Constraints (critical): CA:FALSE
X509v3 Subject Alternative Name:
DNS:web.vm.qstarstar.lab, URI:spiffe://spire.qstarstar.com/vm/web ← 身份 = 这个 URI SANJWT-SVID(同一身份,fetch jwt 取,仅展示解码后的 header/payload):
// header
{"alg":"ES256","kid":"iR9QHBkBgs5MSeOo5Q5UXA4rPmgjQlad","typ":"JWT"}
// payload
{"aud":["nacos"], "sub":"spiffe://spire.qstarstar.com/vm/web", "iat":..., "exp":...}对照
Issuer可见:两个平台的工作负载 SVID 都由同一个自签 CA 签发,因此能被同一个 trust bundle 验证。完整的字段/配置解读见 什么是 SVID 与 三层配置实验。