Compiling a workload registration
A workload registration is never applied as written. AIControls expands its intent tiers into a neutral behavior declaration, compiles that into policy on two planes under guardrails that can only narrow it, and records everything the compilation could not carry across. This page is the reference for what compiles where, what a compile can refuse to do, and the two places a registration is deliberately not fully enforceable.
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.
You see the compile result in two places. The registration wizard's Review step shows what a registration compiles to before you save it. The workload's detail, GET /api/v1/workload-trust/{namespace}/{kind}/{name}, carries the compile result for the registration that is in force. Workload Protection separately reports whether the generated controls are Observing, pending enablement, Enforcing, or Degraded.
Registration intent tiers, such as network: cluster or files: own-volumes, expand to the primitives in the table below using your Runtime Binding. An unbound tier expands to nothing and the workload reports intent-tier-unbound naming the field to configure.
The compile table
Intent has no model or MCP section, so the compile gets no gateway model or tool constraints from intent. Those grants come from Access Bindings.
| Expanded primitive | Compiles to | Plane |
|---|---|---|
network.dns | egress allowlist | Gateway |
network.dns (same values) | RuntimePolicy.network allow, paired with deny: ["*"] | Kernel |
network.dns (negative form) | RuntimePolicy.dns allow — observe only; reports every other name the pod resolves | Kernel |
network.cidrs | RuntimePolicy.network allow, paired with deny: ["*"] | Kernel |
network.ports | part of the gateway allowlist | Gateway only — see below |
exec | RuntimePolicy.exec allow, paired with deny: ["*"] | Kernel (needs the open/exec hooks) |
open.read, open.write | RuntimePolicy.open allow, paired with deny: ["*"] | Kernel (needs the open/exec hooks) |
kubernetes | nothing — it is a posture declaration, checked against the ServiceAccount's real power | — |
One declared name reaches both planes. network.dns is not split between them. The kernel matches a domain from the pod's own DNS answers and a cluster Service name from Service and EndpointSlice informers, so the same name is expressible on both sides. The split is by what each plane can do with a name:
| Gateway | Kernel | |
|---|---|---|
| Granularity | per request — host, path, header, model, tool | per flow — destination |
| Sees the name from | the request itself | the pod's own DNS answer, or a Service informer |
| Needs the workload to cooperate | yes — it must honor the proxy settings | no |
| Enforces | which model, which tool, which prompt | whether a flow may leave at all |
| Blind to | a workload that routes around it | encrypted DNS, a cached answer, an address the pod never resolved |
Neither is a superset of the other, which is why a declared name compiles to both.
Two things a declaration cannot carry into the kernel
Both are recorded as compile findings rather than quietly dropped, because a silent omission is a policy that reads as fully enforced and is not.
No policy value has a port
network.ports reaches the gateway and stops there. There is no kernel policy value that carries a port, so a local model on 11434 or a vLLM server on 8000 is constrained by address or by Service name, or not at all.
You will hit this. A workload that talks to an in-cluster inference server on a non-standard port gets a kernel rule about the destination, and the port half of the declaration is enforced only for traffic that goes through the gateway. Compilation emits compile-ports-dropped naming the exact ports, so the gap appears in the compile findings rather than being discovered from the generated policy.
Ports are still worth declaring. They constrain the gateway plane, and they document intent for review.
The kernel open rule has no read/write mode
open.read and open.write are separate lists in the expanded declaration. The kernel open rule takes one value list, so they are merged: a path you declared write-only compiles as readable too.
This is a genuine widening of the compiled policy relative to what the declaration says, so it is reported as compile-open-mode-collapsed naming the affected paths. The two lists stay separate in the declaration: they are accurate, they are what conformance is checked against, and they are what a future kernel rule with a mode would use.
The four guardrails
1. Clamp, never widen
Every compiled value is intersected with the namespace's platform ceiling before it becomes policy. A declaration can describe a subset of what the platform already allows; it can never open something the platform closed.
A clamp is not a failure. The compile succeeds with the narrower set and records compile-clamped naming what was dropped and what was kept — so "my declared destination is not in the policy" has an answer rather than being a mystery.
2. AIControls renders the policy, not you
A registration is expressed as intent tiers and AIControls renders the enforcement policy from them. There is no field through which a registration can carry Custom Resource text. A registration that could would let the team shipping a workload author the kernel enforcement policy applied to it, which is the wrong direction for a security control.
3. Breadth and sensor representation caps
A registration matching more than a configured share of the filesystem or the address space is rejected at compile, with a message naming the offending patterns and the measured share. It is not narrowed, and it is not compiled — the alternative is a policy that permits everything while reporting as enforced.
| Cap | Default | Rejects | Accepts |
|---|---|---|---|
Filesystem, per open.read / open.write / exec list | 50% | /**, /* | /app/**, /etc/ssl/certs/**, /usr/local/bin/python3.11 |
IPv4 address space, across network.cidrs | 1/256 — one /8 | 10.0.0.0/4, 0.0.0.0/0 | 10.42.0.0/24, 10.42.1.7/32 |
| Runtime sensor CIDR representation, per value | /24 minimum prefix length | 10.0.0.0/23, 10.42.0.0/16, 10.96.0.0/12 | 10.42.0.0/24, 10.42.1.7/32 |
The filesystem share is an estimate, and deliberately a coarse one: a pattern is scored by how many leading path segments it fixes before its first wildcard, so /** scores 100%, /app/** scores about 6%, and an exact path scores nothing. It is not measuring your filesystem — it is separating "permits a directory" from "permits everything". The address-space share is exact.
The sensor limit is separate from breadth. Its current egress maps hold individual IPv4 addresses, so it expands a CIDR into at most the 256 addresses in one /24. Splitting a real cluster /16 into /24s does not make the intent enforceable: it would still require 65,536 address entries. AIControls therefore fails the compile with a diagnostic naming the broad target. It does not render a partial allow list; Enforcement remains Not ready and Verification remains Not verified.
After apply, verification also reads the sensor's own target verdict. Applied=True says the daemon loaded the policy; only TargetsValid=True says every network target was programmed. Both must be true on every compiled runtime policy. A missing, unknown, unresolved, or unsupported TargetsValid condition keeps Enforcement Enable pending and Verification Not verified, with the condition reason shown.
A binding that includes /**, expecting it to be silently reduced to something sensible, is the case this refuses. Narrow the value to the directories the workload actually uses.
4. The 256-name cap on kernel domains
The sensor holds a bounded number of domain names per pod. A compiled allow list past that limit fails loudly rather than being truncated: a truncated allow list denies traffic you declared, which in enforce mode is an outage with no visible cause.
The cap is per pod, not per container, so a multi-container pod is where a realistic declaration trips it.
Cluster Service names cost nothing against this cap. A fully-qualified <service>.<namespace>.svc.cluster.local name resolves from Service and EndpointSlice informers rather than from DNS answers, so prefer it for every in-cluster destination — it is both more precise and free.
The pod is the unit, not the container
The kernel groups by pod. A pod with three containers has three expanded declarations, and the compiled RuntimePolicy is their union — so a sidecar's declared permissions widen the application container's. A mesh proxy alone would blow open any meaningful exec or open allow list.
Three consequences:
- Known infrastructure sidecars are excluded from the union rather than merged, and the compiled policy is stamped with which ones —
pod-level (sidecars excluded: istio-proxy). - Union widening is reported. When merging genuinely grants something a container that constrained that list never declared,
compile-union-widenednames the container, the list, and the extra values. The effective policy being looser than the application asked for is something the operator should be told, not something to discover later. - A pod containing an undeclared container does not compile at all. Not partially — at all. Enforcement becomes Not ready rather than partially protecting the pod. Excluded infrastructure sidecars do not count against this.
A single-container pod is never reported as widened. The warning fires only where a merge actually loosened something.
Observe first, always
Every compiled kernel policy starts in observing mode. Compilation prepares controls; it is not a step in the primary Register → Observe → Enforce workflow.
Enabling enforcement requires an explicit operator action that records the authenticated actor and server time. No configuration flag, source, or score enables a policy on its own.
This is not caution for its own sake. A registration drafted from too short an observation window, or one written for a workload with periodic behavior that did not occur during it, can deny legitimate traffic. Observing shows what would be denied against real traffic before you enable enforcement.
Two further limits on promotion:
- Enablement is refused when there is no registered workload or the readiness review fails. The registration's other preconditions — it names a purpose and an autonomy, and it compiles — are re-evaluated on every reconciliation.
- The negative-form
dnsrule is always monitor, whatever the rest of the policy is doing. It only ever observes.
Compile findings
Every outcome is reported as structured data with a stable code, so it can be displayed, filtered, and acted on rather than read out of a log.
| Code | Blocking | Meaning |
|---|---|---|
compile-undeclared-container | yes | A container in the pod has no declaration, so Enforcement is Not ready rather than partially active |
compile-breadth-cap | yes | A declaration matches more than the configured share of the filesystem or address space |
compile-intern-cap | yes | The compiled domain allow list is past the sensor's per-pod limit |
compile-promotion-refused | yes | Enforcement was requested without a registered workload, a complete server-authored enablement record, or a passing readiness review |
compile-render-failed | yes | The rendered policy failed validation before anything was applied |
compile-clamped | no | Declared values were dropped because the platform ceiling does not permit them |
compile-union-widened | no | The merged pod policy grants something a container never declared |
compile-ports-dropped | no | Declared ports have no kernel rendering |
compile-open-mode-collapsed | no | A write-only path compiled as readable |
compile-sidecar-excluded | no | A container was excluded from the union as infrastructure |
A blocking finding means no policy is produced at all — not a partial one. A partially compiled policy would be a policy nobody wrote. For a running workload, AIControls keeps the policy it applied from the last declaration that compiled until the declaration compiles again or is withdrawn. See A registration that doesn't compile keeps the last working policy.
A registration with nothing kernel-renderable — gateway-only primitives, or tiers that expand to nothing — is not a failure. It compiles the gateway half and reports nothing-to-compile.
Read the compile result
"compile": {
"status": "failed",
"mode": "monitor",
"ceiling": "platform",
"diagnostics": [
{
"code": "compile-breadth-cap",
"severity": "error",
"path": "behavior.open.read",
"detail": "behavior.open.read matches an estimated 100% of the filesystem, over the 50% cap. Broadest first: /** (~100%) …",
"attempted": ["/**"]
}
],
"l7": { "egressDomains": ["api.anthropic.com"], "ports": [443] }
}
status is compiled, nothing-to-compile, or failed. severity is error (blocking), warning, or info; only error sets status to failed. attempted and kept carry the values, so a client can act on them without parsing detail.
See also
- Workload Protection — how the four statuses behave
- Register a workload — declare portable intent tiers
- Configure Workload Protection policy — bind intent tiers to cluster values
- Runtime Controls — the kernel plane the compiled policy runs on
- Network Egress Filtering — the gateway plane's allowlist and how it is clamped