Skip to content

Custom resources

Xila Guard keeps its Kubernetes API surface deliberately small: three administrator-owned custom resources in the group guard.xila.us, version v1alpha1. There is no CRD-per-internal-type; bulky raw evidence stays out of etcd, and decisions go to JSONL + bounded Kubernetes Events instead of CRD status spam.

Kind Owner Purpose
PlacementPolicy administrator Cluster policy: explicit requirements + freshness limits
TenantBinding administrator Maps a Namespace UID to a tenant ID + effective policy
NodeSecurityState controller-written Normalized inventory summary + evidence status for a node

All three are cluster-scoped and use apiextensions.k8s.io/v1. Tenants cannot create or modify any of them — nor the evidence-source configuration (enforced by RBAC).

PlacementPolicy

Administrator-owned cluster policy. The spec.requirements map and freshness limits are exactly the policy model predicates.

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

TenantBinding

Maps a Namespace UID to a tenant ID and the effective policy reference. Using the Namespace UID (not the name) avoids accidentally inheriting trust after a namespace is deleted and recreated — a new namespace with the same name gets a new UID and does not inherit the old binding.

apiVersion: guard.xila.us/v1alpha1
kind: TenantBinding
metadata:
  name: team-alpha
spec:
  namespaceUid: "<Namespace .metadata.uid>"
  tenantId: team-alpha
  effectivePolicyRef: tenant-dedicated-baseline

The authoritative effectivePolicyRef wins over the guard.xila.us/policy Pod annotation. Tenant identity is always server-resolved from this object — never from a tenant-supplied claim.

NodeSecurityState

Written by the controller: a normalized inventory summary and evidence status for a node, keyed by its stable node UID. In the lab this carries small synthetic mock snapshots; with the opt-in identity feature it can reflect SVID-bound authenticated_collector inventory.

apiVersion: guard.xila.us/v1alpha1
kind: NodeSecurityState
metadata:
  name: node-a
spec:
  clusterId: xila-guard-lab
  nodeUid: "<Node .metadata.uid>"
  snapshotRevision: 7
  source: mock          # mock | admin_inventory | host_observation | authenticated_collector
  observedTime: "2026-10-10T00:00:00Z"
  devices:
    - physicalId: GPU-...
      model: ...
      partitioning: none
      timeSlicing: { status: unknown }
      mps: { status: unknown }

Freshness matters

NodeSecurityState snapshots are evaluated against the controller clock. Lab fixtures render a fresh observedTime at apply time so mock evidence stays within the policy's maxAgeSeconds (default 60); a stale snapshot yields EVIDENCE_STALE → indeterminate, never allow.

Installing the CRDs

The CRDs ship with the Helm chart (installCRDs: true by default) and are also available as raw manifests under config/crd/bases in xila-guard-k8s. The generated schemas come from controller-tools (controller-gen v0.19).