Collector identity (SPIFFE / SPIRE)¶
Before a networked collector can be trusted to feed enforcement, the system must know which node actually produced a report — and bind that server-side, not from the request body. Xila Guard implements this with SPIFFE / SPIRE, as an opt-in, default-off feature.
This is authentication, not attestation
SPIFFE attests the node / workload — who produced a report. It is never GPU attestation (that is RATS). The two are independent features, and conflating them is exactly the category error this design avoids.
The binding rule¶
A SPIFFE SVID is an X.509 certificate with a
spiffe://xila.us/node/<node_uid> URI SAN. The core server, started with
--tls-cert-file / --tls-key-file / --client-ca-file, serves HTTPS requiring
a client cert rooted at the CA bundle, extracts the peer SPIFFE ID, and on
POST /v1alpha1/inventory derives the node UID from the verified certificate,
never the body.
sequenceDiagram
participant C as Collector (per-node SVID)
participant S as Inventory receiver (mTLS)
C->>S: POST /v1alpha1/inventory (client SVID: node/<uid>)
S->>S: verify cert, extract SPIFFE ID, bind nodeUid from cert
alt body claims a different node
S-->>C: 403 (IDENTITY_MISMATCH / spoof)
else non-monotonic snapshotRevision
S-->>C: 409 (REPLAY)
else untrusted / unverified cert
S-->>C: TLS handshake rejected
else valid
S-->>C: 200 boundNodeUid=<uid>
end
| Attack | Result |
|---|---|
| spoof — report body claims another node | 403 |
replay — reused / non-monotonic snapshotRevision |
409 |
| untrusted — cert not rooted at the CA bundle | TLS handshake rejected |
A shared DaemonSet service account with a self-chosen node_id is not a valid
node identity — that bypass is exactly what this feature closes.
Authenticated inventory feeds evaluation¶
Authenticated snapshots are attributed to source authenticated_collector and
feed evaluation directly: GET /v1alpha1/inventory/{nodeUid} and
POST /v1alpha1/evaluate-node evaluate against the stored, SVID-bound facts,
fail-closed indeterminate if absent. An enforce context accepts
authenticated_collector while still rejecting mock.
The Kubernetes observer can consume this in-cluster:
--authenticated-inventory=off|prefer|require (prefer falls back to
NodeSecurityState on AUTHENTICATED_INVENTORY_MISSING; require is
fail-closed), gated by the Helm value
identity.observer.useAuthenticatedInventory (default off).
In-cluster: real SPIRE¶
With identity.enabled=true the chart deploys a real SPIRE deployment (not a
mock): a spire-server StatefulSet, a spire-agent DaemonSet using k8s_psat
node attestation, and the SPIFFE CSI driver DaemonSet + CSIDriver. Each consumer
runs a spiffe-helper container that fetches its SVID
(svid.pem / svid_key.pem / bundle.pem) over the SPIFFE CSI Workload API
socket into a shared emptyDir; the file-based mtls binaries consume those
files directly. A dedicated inventory Deployment (with its own SVID) receives
collector reports; the observe / evaluate core is untouched.
flowchart TD
SS[spire-server StatefulSet] --> SA[spire-agent DaemonSet<br/>k8s_psat]
SA --> CSI[SPIFFE CSI driver]
CSI --> SH[spiffe-helper sidecar<br/>svid.pem / key / bundle]
SH --> COL[collector DaemonSet<br/>--features mtls]
SH --> RX[inventory Deployment<br/>--features mtls]
COL -->|mTLS POST| RX
Lab proofs: make demo-identity (host-process openssl SVIDs) and
make demo-identity-cluster (real SPIRE in a 2-node kind cluster — accept /
spoof-403 / replay-409 / untrusted-rejected, plus the observer emitting
outcome=allow source=authenticated_collector and a require fail-closed
AUTHENTICATED_INVENTORY_MISSING on an uninventoried node).
Rotation and HA¶
- SVID rotation: a spiffe-helper daemon sidecar (chart default) rotates
the SVID; the server uses a reloading rustls cert resolver and the agent
re-reads its SVID each POST cycle. Limitation: the client-CA trust bundle is
baked into the rustls
ServerConfigand is not hot-swapped (leaf-only live reload — documented). - HA: observer and scheduler leader election at
replicas > 1.
Bring your own SPIRE¶
If a working SPIRE + SPIFFE CSI driver already runs in the cluster, set
identity.spire.install=false: the SPIRE infrastructure templates are not
rendered and only the consumers deploy, reading SVIDs from the existing Workload
API. Point the values at your installation (identity.spire.trustDomain,
identity.spire.csiDriverName, identity.spire.namespace) and create the
registration entries yourself:
# Per node (collector identity):
spire-server entry create \
-spiffeID spiffe://<td>/node/<node> \
-parentID spiffe://<td>/spire/agent/k8s_psat/<cluster>/<nodeUID> \
-selector k8s:ns:<ns> -selector k8s:sa:xila-guard-collector \
-selector k8s:node-name:<node>
# Inventory receiver identity (DNS SANs let the collector verify the server):
spire-server entry create \
-spiffeID spiffe://<td>/xila-guard/core \
-parentID spiffe://<td>/spire/agent/k8s_psat/<cluster>/<nodeUID> \
-selector k8s:ns:<ns> -selector k8s:sa:xila-guard-inventory \
-dnsName xila-guard-xila-guard-inventory.<ns>.svc
insecure_bootstrap is demo-only. See the
attestation & identity ADR.