Skip to main content

Configure Workload Protection policy

Task

Configure what each workload access level means in your cluster. You set the cluster's Environment defaults once, and you manage Runtime Bindings in Settings > Workloads.

Self-managed installations only

Workload Trust is available only in private, self-managed AIControls installations, on premises or in a cloud cluster that you run. It needs a dedicated cluster that you run, with Kyverno, Kyverno Runtime, and cluster OIDC and RBAC access in place. Nirmata-hosted (SaaS) workspaces don't include it. See Feature availability.

There is no trust floor​

Workload Protection derives Registration, Runtime, Enforcement, and Verification independently. Document source never grants registration or verification, and no document assurance level changes the enforcement ceiling.

How Runtime Bindings work​

A registration uses portable access levels such as network: cluster and files: own-volumes. It does not contain deployment-specific Service names, paths, or interpreters. Two things supply those values:

PieceWhat it holdsWho sets it
Environment defaultsThe cluster's concrete values: cluster Services, gateway Services, expected domains, base files, interpreters, and exec setup files.Your platform team, once per cluster, in Settings > Workloads.
A Runtime BindingThe access levels (network, files, programs with Allow kubectl exec, and Kubernetes API) that a group of workloads runs with, plus optional overrides of any environment value.The team that owns the Blueprints that select it.

Each Workload Blueprint selects exactly one Runtime Binding, and a Runtime Binding can be selected by many Blueprints. A Runtime Binding inherits every environment default. An absent field inherits, and a field you override, even with an empty list, replaces the default for that binding. A workload has no Runtime Binding of its own: Customize for this workload overrides individual access levels, and the Runtime Binding supplies the rest. The environment defaults apply to every cluster. The API also accepts per-cluster entries under environment.clusters, which take the place of the defaults for that cluster.

Set up the bindings​

  1. Open Settings > Workloads. The Environment defaults card shows the cluster's default values. The Runtime Bindings list shows each binding's access levels, how many environment fields it overrides, and which Blueprints use it.
  2. Edit the environment defaults. Select Edit on the Environment defaults card, fill in the lists described in What each binding field controls, and select Preview impact. AIControls shows the runtime-rule change for every workload in the cluster, and you select Confirm and save.
  3. Create a Runtime Binding. Select New in the Runtime Bindings card. The New Runtime Binding drawer opens. Enter a name, set the Access levels, and turn on an override under Environment values only for a field this binding must change. Select Create. The name is fixed after you create it. You can also create a binding while you create a Blueprint. See Choose or create the Runtime Binding.
  4. Select it in a Blueprint. In the Blueprint editor's Runtime section, choose Use an existing Runtime Binding.

Every save is checked for valid values (an invalid value returns 400 invalid_runtime_binding, or 400 invalid_environment for the environment defaults), recorded in the configuration audit trail, and recompiles the affected workloads' RuntimePolicies on the next reconcile. If a level has no value in the cluster's environment, it expands to no rule and the workload reports intent-tier-unbound with the field to fill in.

Changing a binding affects enforcing workloads

A binding change applies to every workload whose Blueprint selects it, including workloads whose enforcement is enabled. A narrower list can block traffic those workloads rely on. The impact preview lists every Blueprint and workload, marks the runtime-enforced ones, and shows each one's runtime-rule diff, computed against its running pods now. Review it before you confirm.

Review history and restore​

Each Runtime Binding keeps a history of its changes. Open a binding's actions menu and select History to see who changed it and when, to compare a revision with the current binding, and to Restore it. A restore goes through the same impact preview as an edit, you select Confirm and restore, and it appends a new revision. The environment defaults keep their history in the same way: select History on the Environment defaults card. See Review and restore a binding change.

Delete a Runtime Binding​

You can't delete a Runtime Binding that a Blueprint selects. To delete one, open its actions menu and select Delete. AIControls refuses the delete with 409 binding_in_use and lists the Blueprints. Select a different Runtime Binding in each of those Blueprints, and then delete it.

The default environment values​

A fresh install starts with a narrow set of environment defaults that covers what nearly every containerized process needs, and nothing broader:

FieldDefault
Cluster serviceskube-dns.kube-system.svc.cluster.local
AIControls gatewayThis install's AIControls Service, for example aicontrols.aicontrols.svc.cluster.local
Expected domains*.cluster.local
Base files/etc/hostname, /etc/hosts, /etc/resolv.conf, /etc/nsswitch.conf, /etc/passwd, /etc/group, /etc/localtime, /etc/ld.so.cache, /etc/ssl/**, /usr/share/zoneinfo/**, /usr/lib/**, /lib/**, /usr/local/lib/**, /dev/null, /dev/urandom
Interpreters/bin/sh, /usr/bin/sh, /usr/bin/dash, /usr/bin/python3, /usr/local/bin/python3, /usr/local/bin/node

It leaves out the Kubernetes API, /proc, writable scratch space such as /tmp, bash, and version-specific runtime paths. Give a workload that needs them a Runtime Binding that overrides those fields, so the defaults never grant them to every workload. Some runtimes read /proc/self/**; test in monitor mode before you enforce a files tier.

Seed the environment defaults from Helm​

On first install, you can seed different environment defaults from Helm values. Setting any value replaces the default entirely. After the first start, the database is authoritative: edit the environment defaults in Settings > Workloads, because changing these values has no effect.

workloadTrust:
runtimeBinding:
clusterServices:
- kubernetes.default.svc.cluster.local
- kube-dns.kube-system.svc.cluster.local
gatewayServices:
- aicontrols.aicontrols.svc.cluster.local
expectedDomains:
- aicontrols.aicontrols.svc.cluster.local
baseFiles:
- /etc/ssl/certs/ca-certificates.crt
- /etc/os-release
interpreters:
- /bin/sh
- /usr/bin/python3
execSetupFiles: # optional; these five are the default
- /etc/passwd
- /etc/group
- /etc/nsswitch.conf
- /proc/*/setgroups
- /proc/*/task/*/**
Use exact Service names or narrow CIDRs

The runtime sensor rejects CIDRs wider than /24, and AIControls refuses to save one. Prefer exact Service FQDNs, such as kubernetes.default.svc.cluster.local. clusterServices is an explicit allow list. It does not discover or permit every Service in the cluster automatically.

What each environment field controls​

These fields are the environment defaults. A Runtime Binding can override any of them.

FieldUI labelTier expansion
clusterServicesCluster servicesnetwork: cluster expands to these exact cluster destinations. network: llm-providers includes them too.
gatewayServicesAIControls gatewaynetwork: llm-providers adds these AIControls gateway Services. Don't list provider hostnames, because direct provider access bypasses the gateway.
expectedDomainsExpected domainsnetwork: internet stays unrestricted, and DNS observation reports names outside this set. DNS is observed only, never blocked.
baseFilesBase filesfiles: none expands to these files. files: own-volumes adds the container's volume mounts and working directory, each as <dir>/**. Both tiers also add the container's AIControls token mount automatically, so don't list it here. See Path values and the sensor path grammar.
interpretersInterpretersexec: shell adds these shells and language runtimes to the container entrypoint. exec: none permits only the entrypoint. Use absolute paths. Allowing kubectl exec also adds them to the exec allow list.
execSetupFilesExec setup filesA workload that sets exec.allowKubectlExec adds these files to its open allow list when its files tier compiles a rule. They are the files the container runtime opens while it sets up a kubectl exec session. When the binding leaves this field empty, AIControls uses /etc/passwd, /etc/group, /etc/nsswitch.conf, /proc/*/setgroups, and /proc/*/task/*/**. Values follow the same sensor path grammar as baseFiles. /proc/*/setgroups and /proc/*/task/*/** use the procfs PID and thread-ID wildcards, which match those entries for any process in the container, not only the kubectl exec session. With a sensor older than v0.1.4, leave both out: the sensor can't match them, and an unmatched value leaves the whole files rule uncompiled. See kubectl exec is blocked by default.

Network none compiles the sensor's deny-everything network shape and needs no binding. The Kubernetes level uses the value in the workload's effective intent rather than an environment field.

Path values and the sensor path grammar​

files: and exec: tiers compile to path values that the runtime sensor matches in the kernel. Write each baseFiles and interpreters value in one of two forms:

ValueMeaning
/etc/os-releaseOne exact file.
/usr/local/lib/python3.12/**The directory and everything under it, at any depth.
/proc/*/setgroupsThe same file under every process's /proc/<pid>/ directory. See The procfs PID wildcard.

The deployed sensor decides whether a directory value can be enforced. Tell AIControls which sensor you run with runtimeControls.sensorPathGrammar:

sensorPathGrammarSensorWhat compiles
directory-prefix (default)kyverno-runtime v0.1.3 or later. The chart bundles v0.1.4.Exact paths, and <dir>/** rendered as <dir> plus <dir>/*. The sensor matches <dir>/* against everything under the directory at any depth, but not the directory itself, so AIControls also renders <dir>.
exactkyverno-runtime v0.1.2 or earlierExact paths only. The sensor matches every value as a literal string, so a directory value never matches a file.

AIControls can't read the sensor's version or grammar from the cluster: no sensor status condition reports either one. Choose the value yourself:

  • With the bundled sensor, keep the default.
  • With your own sensor (kyverno-runtime.enabled: false), set exact if that sensor is older than v0.1.3. On such a sensor the rendered directory prefixes match nothing, so an Enforce-mode policy would deny the workload's own files.
  • An unknown value falls back to exact and AIControls logs a warning at startup.

A directory-prefix sensor looks up a directory only within the first 16 path separators of a file it observes, so AIControls compiles no rule for a directory more than 15 components deep. It also refuses a value that ends in /, a * anywhere other than a trailing /** or the PID segment of a /proc/*/ path, and a directory value that covers the whole filesystem (/**).

AIControls also refuses /proc/**. It would render as the directory prefix /proc/*, which kyverno-runtime v0.1.4 and later reject because a * directly under /proc/ is the PID wildcard, and the sensor refuses the whole policy with one rejected value. List the procfs paths the workload needs instead, such as /proc/*/setgroups or /proc/sys/**. If your binding has /proc/** from an earlier release, the files rule doesn't compile until you replace it, and the workload's files tier shows a note that names the value.

The procfs PID wildcard​

Files under /proc/<pid>/ have a different path for every process, so an exact value can't name them. With kyverno-runtime v0.1.4 or later and the directory-prefix grammar, write the PID as *, as in /proc/*/setgroups. AIControls renders the value unchanged, and the sensor matches the * against any numeric PID, not only the process that opens the file. An allow therefore covers that file for every process in the container.

The * is accepted only as the whole PID segment directly under /proc/, followed by a file path with no other wildcard. The one exception is /proc/*/task/*/**, whose second * is the thread ID: it covers each thread's procfs directory, renders as the directory prefix /proc/*/task/*, and is accepted only as an execSetupFiles value for a workload that allows kubectl exec. A Runtime Binding refuses it in baseFiles. AIControls refuses every other form, such as /proc/*/task/*/fd, /proc/*/task/*/setgroups, /proc/*/**, /proc/1*/setgroups, and /usr/*/setgroups, and the exact grammar refuses both wildcards. A refused value leaves the whole files rule uncompiled, with a note that names the value.

Before AIControls renders a policy, it checks every compiled path value against this grammar. When a tier holds a value the sensor wouldn't match as intended, AIControls compiles no rule for that tier and reports why, rather than rendering an allow list that would deny the workload's own files in Enforce mode and report every library load in Monitor mode:

  • The registration wizard's Review step shows the tier with no values, an unbound tag, and the reason.
  • The workload's expansion notes carry intent-sensor-path-unmatched, and the compile result carries compile-sensor-path-unmatched, each naming the values.

With an exact sensor, a files tier compiles no open rule only when its expanded values contain a directory value: a directory in baseFiles, or, for files: own-volumes, a volume mount or working directory on the pod. AIControls then neither enforces nor observes that tier. A tier whose expanded values are all exact paths still compiles: files: none with an exact-file baseFiles list, or files: own-volumes with an exact-file baseFiles list on a pod with no volume mounts and no working directory. To enforce directory values, upgrade the sensor to v0.1.3 or later and use directory-prefix.

AIControls never renders a one-level pattern such as /etc/ssl/*. A directory-prefix sensor reads a trailing /* as any depth, so rendering it would widen the allow list. Write /etc/ssl/** or list the files.

The AIControls token mount is always readable​

An agent authenticates to AIControls with a projected ServiceAccount token. For each container, AIControls finds the volumes that project a ServiceAccount token whose audience is the cluster OIDC audience (identity.clusterOIDCAudience, aicontrols by default) and adds each mount to the file allow list of both files: none and files: own-volumes. A directory mount becomes <mount>/**. A subPath mount of the token file becomes that file. The Review step names the token mount in the tier's source. A projected token with another audience isn't added.

You don't add the token path to baseFiles. If an older binding lists /run/secrets/aicontrols/** by hand, you can remove it.

AIControls reads the audience on every pass: a cluster OIDC audience you set in Settings takes effect without a restart and overrides identity.clusterOIDCAudience.

With an exact sensor, a directory mount can't match the kernel-resolved ..data/token target, so AIControls leaves it out of the files tier and reports intent-token-mount-not-matchable. The tier still compiles, but enforcing it would deny the workload's own authentication, so the readiness rule for the RuntimePolicy shows Fail until you fix the mount. A mount with a subPathExpr, or a whole mount of a projected volume that holds other sources besides the token (a ConfigMap, a Secret, or a second token), can't be turned into a safe path, so AIControls leaves it out of the files tier, never allows the other sources, and reports intent-token-mount-not-matchable. AIControls also drops a working directory at or below such a mount from files: own-volumes, in both the read and write lists, because <workingDir>/** would allow the mount again. Volumes you mount separately stay, including ones mounted below the token mount. Mount the token with a fixed subPath, or in a projected volume of its own. A subPath mount of the token file is an exact path, so it stays, and files: own-volumes doesn't also expand it to <file>/**. To cover a directory mount, upgrade the sensor and set runtimeControls.sensorPathGrammar to directory-prefix.

Paths under /var/run also compile as /run​

On most nodes /var/run is a symlink to /run, and the kernel reports the /run path. For every baseline, workspace, or token path that starts with /var/run/, AIControls also compiles the /run/ form, so a token mounted at /var/run/secrets/aicontrols/token matches. AIControls adds no other widening.

Exec tiers need an absolute entrypoint​

exec: none and exec: shell both include the container's entrypoint: command[0] from the pod spec of the workload's running pods. When the pod sets no command, AIControls reads the entrypoint from the image config instead (see Image entrypoint and symlinks). The Review step and the compiled policy use the same pods and the same cached image reads. While the image config read is in progress, or when it fails, AIControls treats an image entrypoint as unknown, so the Review step and the compiled policy don't differ by a half-read image. When the config is read but the layer scan fails, AIControls compiles the declared entrypoint path and reports intent-entrypoint-symlink-unresolved. A retry after a failed read counts as in progress. For a workload whose enforcement is enabled, AIControls holds the promotion while any of these is pending: a container's program is unknown or is a relative path, an image symlink scan runs or fails, or the token mount can't be matched. The hold applies to the first enforce-mode policy as well as to later ones, so AIControls never creates enforcement that has no exec rule, names only a symlink path, or blocks the workload's own authentication. With a policy already applied, it stays unchanged. The promotion applies in the first reconcile pass after the resolution completes. With runtimeControls.disableImageReads set, a command in the pod spec is authoritative and carries no symlink warning. When a rollout changes the command, the next reconcile pass recompiles.

The sensor matches kernel-resolved absolute paths, so set an absolute command in the pod spec, such as ["/usr/bin/sleep", "infinity"]. When AIControls can't determine an absolute entrypoint, because the pod sets a relative command such as sleep, or sets none and the image can't be read, it compiles no exec rule for the workload and reports intent-tier-unbound with the reason. An allow list without the entrypoint would stop the container on its next restart. The readiness rule for the RuntimePolicy depends on the cause. A relative command is a mistake in the pod spec, so the rule shows Fail. When the image can't be read and the pod sets no command, the rule shows Review, not Fail, so an automatic registration isn't demoted. Set an absolute, symlink-resolved command, such as /usr/bin/python3.15, to clear either one. A relative command also blocks enforcement: the enforcement review isn't ready, and the enable action refuses it with a reason that tells you to set an absolute command.

Image entrypoint and symlinks​

When a pod sets no command, AIControls reads the image config for the container's image digest, as reported in the pod's container status. It uses Entrypoint[0], or Cmd[0] when the image sets no entrypoint and the pod sets no args, but only when the path is absolute. The read is anonymous and runs in the background. AIControls caches it by digest, backs off after a failure, and never reads a Secret. AIControls reads images only for registered workloads and for the workload you review in the registration wizard, never for other pods in the cluster. The cache keeps the entrypoint and the resolved target of each path, not the image's symlink table.

The read is an outbound request from the AIControls pod to the image's registry, so allow that egress in your NetworkPolicy. Anonymous Docker Hub pulls count against the rate limit of the pod's egress IP. AIControls refuses a registry address that is loopback, link-local, unspecified, multicast, or a cloud metadata address (including fd00:ec2::254, 100.100.100.200, 192.0.0.192, and 168.63.129.16), including the token endpoint a registry names. For a multi-arch image, it reads the linux image for the architecture of the node the pod runs on: the node's kubernetes.io/arch label, or the architecture the pod spec pins with a nodeSelector or a single-valued required node affinity. When AIControls can't establish the architecture, it doesn't read the image, the entrypoint stays unresolved, and an enforced policy is held. The cache keys each read by image digest and architecture. To turn the reads off, set runtimeControls.disableImageReads to true (the Helm value runtimeControls.imageReads.enabled: false). Then AIControls compiles an exec rule only from a pod-spec command, and an image entrypoint stays unknown.

While the symlink scan runs, which takes up to 90 seconds and starts again after an AIControls restart, an image entrypoint stays unknown, and a pod-spec command is marked as having unresolved symlinks, so readiness shows Review and an enforced policy is held until the scan finishes. The next pass picks up the resolved path. If AIControls can't read an image's config (a private, air-gapped, or node-loaded image), its entrypoint stays unknown until you set command.

The kernel reports the program after it follows symlinks, for example /usr/bin/python3 resolves to /usr/bin/python3.15. When the image is readable, AIControls follows up to eight symlinks through the image layers and compiles both the declared path and its target, for the image entrypoint and for a pod-spec command[0]. When it reads the image config but can't scan the layers, it compiles the declared path only and reports intent-entrypoint-symlink-unresolved. A pod-spec command is also authoritative, with no warning, when runtimeControls.disableImageReads is on or when the image config stays unreadable after a failed read. If that command is a symlink, set it to the resolved path. If your entrypoint is a symlink and the image is private, set command to the resolved path.

Fact-dependent tiers need a complete pod observation​

exec: none, exec: shell, files: none, and files: own-volumes expand from the running pods. The files: none tier reads the pods for the token mount. While the pod observation on a replica is incomplete, for example just after a restart, or while a pod that carries the workload's labels isn't yet attributed to its owner, for example a new rollout pod, AIControls doesn't compile these tiers. A list built from only some of the pods could omit another pod's entrypoint or mounts. The workload reports intent-tier-facts-incomplete, the Review step shows the tier with no values, and an already-applied policy stays unchanged until a complete observation is available.

When no running pod is attributed to a registered workload, for example while it is scaled to zero, mid-rollout, or not yet attributed after a restart, AIControls doesn't recompile it and keeps any already-applied policy rather than removing it. The workload reports intent-policy-held. AIControls removes the policy only after you withdraw the registration. It also removes no compiled policy in a reconcile pass that can't attribute every running pod to its owner, so a stale or failed attribution never deletes enforcement.

A registration that doesn't compile keeps the last working policy​

A saved registration can stop compiling, for example when a binding names a network target wider than /24 or a value the sensor can't render. AIControls then keeps the policy it applied from the last registration that compiled. It doesn't delete or rewrite that policy, so a typo never turns protection off.

While the policy is held, the workload reports the reason code enforcement-control-generation-failed-policy-held:

  • Enforcement is Degraded and Verification is Not verified. The enforced rules come from the last working registration and don't match the saved one, so the workload is never Protected against the new registration.
  • The console shows "Degraded, registration doesn't compile — previous policy kept." The status reason names the blocking compile error and says whether the kept rules are enforced, observed in Monitor mode, not yet confirmed as applied by the sensor, or edited since AIControls applied them.
  • The workload's expansion notes carry intent-policy-held-compile-failed with the same compile error.

Holding never outlasts a decision to stop enforcing. If you disable enforcement while the registration still doesn't compile, or while AIControls can't recompile it because no pod is attributed or the pod observation is incomplete, AIControls removes the held Enforce-mode policy in the next reconcile pass that attributes every running pod, instead of keeping it enforcing, and keeps only the observation policies. A registration that compiled would have switched the policy to Monitor mode, and a registration that doesn't compile can't.

To recover, fix the registration or the binding. The next reconcile pass compiles the registration and replaces the held policy. If you withdraw the registration instead, AIControls removes the held policy. A workload that never had a policy applied reports enforcement-control-generation-failed, because there are no previous rules to keep.

RuntimePolicies AIControls manages​

AIControls changes and deletes a RuntimePolicy only when it carries the app.kubernetes.io/managed-by: aicontrols label and the owner-id of the AIControls component acting on it. Each policy AIControls creates carries these labels:

LabelValueMeaning
app.kubernetes.io/managed-byaicontrolsAIControls owns the object.
aicontrols.nirmata.io/owner-idaicontrols-workload-trust for workload protection and kill switch policies; the installation ID for runtime controls policies, or aicontrols if the installation ID couldn't be readWhich AIControls component owns the object. Each component updates and removes only objects with its own owner-id.
aicontrols.nirmata.io/intentrestrict, observe, killswitch, or capability-probeWhat the policy is for.

You can ship your own RuntimePolicies with your workloads, including policies that select the same pods as a registered workload. Leave out the app.kubernetes.io/managed-by: aicontrols label, or set it to your own tool's name, and don't copy AIControls' owner-id label. AIControls doesn't rewrite or delete a RuntimePolicy without both labels in any reconcile pass, compile failure, registration withdrawal, or kill switch cleanup. If your policy already holds a name AIControls would render, AIControls leaves your policy in place and reports the name collision instead of applying its own.