Skip to content

Getting started

This page gets you from a clean checkout to decisions on your screen in a few minutes — no GPU and no Kubernetes required for the first path.

Prerequisites

The offline demo needs only a Rust toolchain plus the xila-guard-core checkout. The Kubernetes demos add Go, a container runtime, kind, Helm, and kubectl. These are the versions the project is actually tested against:

Tool Version Needed for
Rust (stable) 1.93.0 building core binaries (all demos); pinned via rust-toolchain.toml
Go 1.26.1 controller / scheduler / webhook (demo-kind, enforcement, make check)
Container runtime podman 4.9.3 (docker-compat) kind uses KIND_EXPERIMENTAL_PROVIDER=podman; Docker should also work
kind 0.31.0 node image Kubernetes 1.35.0 (M3 / in-cluster); 0.27 for the earlier observe demo
Helm 4.1.4 chart lint / template / install
kubectl 1.36.0 (client) compatible with 1.35 clusters
make / git / jq GNU Make 4.3 / git 2.43 / jaq 2.3 (jq-compatible) demos + checks
Host OS Linux (dev host: WSL2) cgroup v2 required for the Kubernetes 1.35 kind clusters

Pin one Kubernetes version per deployment

The enforcement scheduler is built from k8s.io/kubernetes v1.35.3 and the cluster node image must match (1.35). A skewed apiserver (e.g. 1.32) does not serve resource.k8s.io/v1 (DRA went GA in 1.35) and the scheduler's informers never sync. See the support matrix.

Clone

The three repositories are sibling checkouts; the lab expects xila-guard-core and xila-guard-k8s next to it.

git clone https://github.com/xilasec/xila-guard-core.git
git clone https://github.com/xilasec/xila-guard-k8s.git
git clone https://github.com/xilasec/xila-guard-lab.git

The fast demos

Everything is driven from xila-guard-lab through a small make interface.

cd xila-guard-lab
make demo-offline

Builds the core binaries and runs xila-guard evaluate against shared fixtures for an allow (exit 0), a deny (exit 2), and an indeterminate (exit 3) scenario, plus xila-guard policy validate on a valid and an invalid policy. Prints a PASS/FAIL summary and exits non-zero on any mismatch.

make demo-kind      # disposable single-node kind cluster, observe mode
make clean-kind     # deletes ONLY the xila-guard-lab cluster + ./out

Creates a disposable, CPU-only kind cluster, applies the CRDs plus synthetic mock inventory, runs the observer once, and shows allow / deny / indeterminate decisions and a drift event. Every output line visibly says mode: observe and source: mock.

make demo-m3        # 3-node kind v0.31 / k8s 1.35; SIMULATED over mock

Runs the controlled scheduler + fail-closed webhook + RBAC. See enforcement. This is simulated software integration over mock evidence — not real GPU isolation.

make demo-nvidia    # real nvidia-smi on this host + simulated A100/MIG

Collects the host GPU via read-only nvidia-smi (host_observation) and evaluates it; unknown sharing facts yield indeterminate, never allow. Then exercises a clearly-labeled simulated A100 + MIG profile.

More one-command paths: make demo-incluster (build images + Helm install), make demo-attestation, make demo-identity (host-process SVIDs), make demo-identity-cluster (real SPIRE), make bench, and make supply-chain.

Full in-cluster walkthrough

This is the complete path: install the operator with Helm, declare a policy, describe what is on your nodes, bind a namespace to a tenant, run a workload, and watch the decisions appear — first in observe mode, then with opt-in enforcement.

One command does all of this

cd xila-guard-lab && make demo-incluster builds the images, creates a disposable kind cluster, installs the chart, and runs this entire observe + enforcement flow with PASS/FAIL assertions. The steps below are the same flow, unpacked, so you can run it against your own cluster.

Images are built from source (nothing is published yet)

The chart image defaults are placeholders (localhost/xila-guard-*:dev). There is no public registry yet, so you either build + load the images into your cluster (shown below for kind) or build + push them to your own registry and set --set coreServer.image.repository=... etc. The commands never require secrets.

1. A cluster and the images

# A disposable, CPU-only cluster (no GPU, no device plugin). Any 1.35 cluster works.
kind create cluster --name xila-demo

# Build the two observe-mode images from source and load them into the cluster.
podman build -t localhost/xila-guard-server:dev \
  -f xila-guard-core/Dockerfile xila-guard-core
podman build --build-arg COMPONENT=controller \
  -t localhost/xila-guard-controller:dev xila-guard-k8s

for img in xila-guard-server xila-guard-controller; do
  podman save -o "/tmp/$img.tar" "localhost/$img:dev"
  kind load image-archive "/tmp/$img.tar" --name xila-demo
done

2. Install the operator (observe mode)

helm install xila-guard xila-guard-k8s/charts/xila-guard \
  --namespace xila-guard-system --create-namespace \
  --set coreServer.image.pullPolicy=IfNotPresent \
  --set controller.image.pullPolicy=IfNotPresent \
  --wait --timeout 180s

kubectl get pods -n xila-guard-system

This installs the guard.xila.us CRDs, the core evaluator service, and the observer. mode: observe is the default: nothing is scheduled, blocked, or evicted — the observer only reports decisions.

3. Declare a policy

A PlacementPolicy is the tenant's security requirements. Its spec is forwarded verbatim to the Rust evaluator — Kubernetes never reinterprets it.

kubectl apply -f - <<'EOF'
apiVersion: guard.xila.us/v1alpha1
kind: PlacementPolicy
metadata:
  name: tenant-dedicated-baseline
spec:
  placementScope: node
  requirements:
    allowedRegions: [eu-central-1]
    nodeTenantAssignment: sameTenant
    gpu:
      timeSlicing: forbidden
      mps: forbidden
      allowedPartitioning: [none]
      approvedDriverVersions: ["fixture-driver-1"]
      attestation: notRequired
  evidence:
    maxAgeSeconds: 60
    maxClockSkewSeconds: 5
EOF

attestation: notRequired is not a guarantee

notRequired means the predicate is not requested — it is not evidence that the property holds. See attestation.

4. Describe what is on the nodes

A NodeSecurityState is the evidence snapshot for a node, keyed by its node UID. Here we declare two synthetic (source: mock) nodes — one compliant, one time-sliced — with fresh timestamps so they satisfy evidence.maxAgeSeconds.

NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
for spec in "nss-node-allow node-uid-allow false" "nss-node-deny node-uid-deny true"; do
  set -- $spec; NAME=$1 UID=$2 TS=$3
  kubectl apply -f - <<EOF
apiVersion: guard.xila.us/v1alpha1
kind: NodeSecurityState
metadata: { name: ${NAME} }
spec:
  clusterId: cluster-xila-demo
  nodeUid: "${UID}"
  nodeIncarnation: inc-1
  snapshotRevision: rev-1
  source: mock
  sourceIdentity: mock-provider/${NAME}
  complete: true
  sourceErrors: []
  region: { status: known, value: eu-central-1, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
  assignedTenant: { status: known, value: tenant-a, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
  devices:
    - vendor: nvidia
      physicalId: GPU-${NAME}-1
      pciAddress: "0000:00:1e.0"
      configGeneration: 1
      model: { status: known, value: fixture-model-A, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
      driverVersion: { status: known, value: fixture-driver-1, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
      firmwareVersion: { status: unknown, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
      sharing:
        partitioning: { status: known, value: none, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
        timeSlicing: { status: known, value: ${TS}, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
        mps: { status: known, value: false, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
        virtualization: { status: known, value: none, source: mock, sourceIdentity: m, observedTime: "${NOW}", receivedTime: "${NOW}" }
      supportedCapabilities: [mig, attestation]
EOF
done

source: mock is simulated evidence

In production these snapshots come from the discovery provider (real nvidia-smi) or an authenticated collector — never hand-written mock. An enforcement/production profile rejects mock outright.

5. Bind a namespace to a tenant

The TenantBinding maps a namespace's UID (not its name) to a tenant and the policy that governs it. It is administrator-owned; tenants must not edit it.

kubectl create namespace xila-guard-demo

kubectl apply -f - <<'EOF'
apiVersion: guard.xila.us/v1alpha1
kind: TenantBinding
metadata:
  name: tenant-a-binding
spec:
  namespaceUid: "__NAMESPACE_UID__"
  tenantId: tenant-a
  effectivePolicyRef: tenant-dedicated-baseline
EOF

# Bind on the real UID (survives a namespace delete+recreate):
NS_UID=$(kubectl get ns xila-guard-demo -o jsonpath='{.metadata.uid}')
kubectl patch tenantbinding tenant-a-binding --type=merge \
  -p "{\"spec\":{\"namespaceUid\":\"${NS_UID}\"}}"

6. Run workloads that opt in

A Pod opts into evaluation with annotations. These are CPU-only pods — they request no nvidia.com/gpu and nothing is scheduled by Xila Guard in observe mode.

for p in "pod-allow node-uid-allow" "pod-deny node-uid-deny"; do
  set -- $p; NAME=$1 NODEUID=$2
  kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: ${NAME}
  namespace: xila-guard-demo
  annotations:
    guard.xila.us/evaluate: "true"
    guard.xila.us/policy: "tenant-dedicated-baseline"
    guard.xila.us/candidate-node-uid: "${NODEUID}"
    guard.xila.us/candidate-scope: "node"
spec:
  restartPolicy: Never
  containers:
    - name: pause
      image: registry.k8s.io/pause:3.10
      resources: { requests: { cpu: "10m", memory: "16Mi" }, limits: { cpu: "50m", memory: "32Mi" } }
EOF
done

7. Verify it works

The observer records each decision as a Kubernetes Event and a JSONL line.

kubectl get events -n xila-guard-demo | grep -i Placement

Expected — the compliant node is allowed, the time-sliced node is denied with a machine-readable reason:

Normal    PlacementAllow   pod/pod-allow   observe/mock: outcome=allow node=node-uid-allow reasons=[]
Warning   PlacementDeny    pod/pod-deny    observe/mock: outcome=deny  node=node-uid-deny  reasons=[TIME_SLICING_FORBIDDEN]
# The observer's own log (label selector — no need to know the pod name):
kubectl logs -n xila-guard-system -l app.kubernetes.io/component=observer --tail=20

A node whose evidence is missing or stale yields indeterminate, never allow — that is the fail-closed guarantee in action.

8. (Optional) Turn on enforcement

Enforcement is opt-in, experimental, and — in this walkthrough — simulated over mock evidence (not real GPU isolation). It adds a controlled scheduler and a fail-closed admission webhook. Build + load those two images first:

podman build --build-arg COMPONENT=scheduler -t localhost/xila-guard-scheduler:dev xila-guard-k8s
podman build --build-arg COMPONENT=webhook   -t localhost/xila-guard-webhook:dev   xila-guard-k8s
for img in xila-guard-scheduler xila-guard-webhook; do
  podman save -o "/tmp/$img.tar" "localhost/$img:dev"
  kind load image-archive "/tmp/$img.tar" --name xila-demo
done

The webhook needs TLS for its in-cluster Service DNS (self-signed = its own CA):

SVC=xila-guard-xila-guard-webhook           # <release>-<chart>-webhook
SAN="DNS:${SVC}.xila-guard-system.svc,DNS:${SVC}.xila-guard-system.svc.cluster.local"
openssl req -x509 -newkey rsa:2048 -nodes -days 2 \
  -keyout /tmp/tls.key -out /tmp/tls.crt \
  -subj "/CN=${SVC}.xila-guard-system.svc" -addext "subjectAltName=${SAN}"

kubectl create secret tls xila-guard-webhook-tls -n xila-guard-system \
  --cert=/tmp/tls.crt --key=/tmp/tls.key
CA_BUNDLE=$(base64 -w0 < /tmp/tls.crt)

helm upgrade xila-guard xila-guard-k8s/charts/xila-guard \
  --namespace xila-guard-system \
  --set enforcement.enabled=true \
  --set enforcement.scheduler.acceptMock=true \
  --set enforcement.webhook.tls.caBundle="${CA_BUNDLE}" \
  --wait --timeout 180s

Mark the namespace protected, label a real node into the tenant pool, and give that node a compliant snapshot:

kubectl label namespace xila-guard-demo guard.xila.us/protected=true --overwrite
WORKER=$(kubectl get nodes -l '!node-role.kubernetes.io/control-plane' -o jsonpath='{.items[0].metadata.name}')
[ -z "$WORKER" ] && WORKER=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
WUID=$(kubectl get node "$WORKER" -o jsonpath='{.metadata.uid}')
kubectl label node "$WORKER" guard.xila.us/tenant-pool=tenant-a --overwrite
# Re-apply the NodeSecurityState loop from step 4 using NAME=nss-pool-worker UID="$WUID" TS=false

A compliant Pod routed to the controlled scheduler is bound; a Pod that tries to bypass it (default scheduler, or a direct nodeName) is denied by the fail-closed webhook:

# Compliant — scheduled by xila-guard-scheduler:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: enforced-allow
  namespace: xila-guard-demo
  annotations: { guard.xila.us/policy: "tenant-dedicated-baseline" }
spec:
  schedulerName: xila-guard-scheduler
  restartPolicy: Never
  containers:
    - name: pause
      image: registry.k8s.io/pause:3.10
      resources: { requests: { cpu: "10m", memory: "16Mi" }, limits: { cpu: "50m", memory: "32Mi" } }
EOF
kubectl get pod enforced-allow -n xila-guard-demo -o wide        # -> bound to $WORKER

# Bypass attempt — rejected at admission:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: enforced-bypass, namespace: xila-guard-demo }
spec:
  restartPolicy: Never
  containers:
    - name: pause
      image: registry.k8s.io/pause:3.10
EOF
# Error from server: admission webhook "validate-pods.guard.xila.us" denied the request:
#   xila-guard: protected pod must use schedulerName "xila-guard-scheduler" ...

See enforcement for the full fail-closed matrix.

9. Clean up

helm uninstall xila-guard -n xila-guard-system
kind delete cluster --name xila-demo

Evaluate: exit codes

The xila-guard evaluate CLI maps the decision algebra onto process exit codes, so it composes cleanly in scripts and CI:

Exit code Meaning
0 allow — all required predicates satisfied
2 deny — at least one required predicate violated
3 indeterminate — nothing violated, but at least one predicate unresolved
1 error — malformed input or execution failure (never a security decision)
cd xila-guard-core && cargo build --bins
B=target/debug
$B/xila-guard policy validate --file fixtures/policies/tenant-dedicated-baseline.yaml
$B/xila-guard evaluate --request fixtures/requests/allow_all_satisfied.json                 # exit 0
$B/xila-guard evaluate --request fixtures/requests/deny_time_slicing.json                   # exit 2
$B/xila-guard evaluate --request fixtures/requests/indeterminate_attestation_required.json  # exit 3
$B/xila-guard inventory --provider nvidia      # real discovery, or structured "unsupported" — never a crash
$B/xila-guard-server --listen 127.0.0.1:8080   # bounded loopback API

Machine-readable output goes to stdout; diagnostic logs go to stderr. A missing NVIDIA library is a structured unsupported-provider result, not a crash.

Repo-wide checks

cd xila-guard-lab
make check   # core fmt/clippy/test + go vet/test + helm lint + offline assertions
make help    # list all targets

Next: read the policy model to understand what the evaluator actually checks, or jump to architecture.