Skip to content

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 ServerConfig and 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.