Nirmata created Kyverno and donated it to the CNCF in 2020. Kyverno is now a graduated CNCF project and is widely used for enforcing and automating configuration security and best practices.
However, admission control answers one question: does this spec look right? It cannot answer the question that matters once the pod is running: is this workload doing what its spec says it should? With autonomous AI agents and non-deterministic LLM-enabled applications, that gap is where most of the interesting incidents live.
A container with an innocuous-looking image pulls a second-stage payload after startup. A sidecar with network access nobody scoped tightly enough starts talking to a destination no one approved. A process no configuration policy anticipated executes, because the policy only ever described the container’s declared behavior, not its runtime one.
Nirmata Runtime for Kyverno closes that gap by moving enforcement into the kernel, on the node, in the same path as the syscall or packet itself — not in a sidecar that can be starved of CPU, not in a proxy that can be routed around, not in a controller that finds out about a bad exec only after reading a log line written after the fact.
While other runtime projects exist, none offer both the visibility and kernel enforcement required for newer AI applications with clean integration and reuse of existing components, such as OpenReports and CEL libraries used in Kubernetes and Kyverno.
Below the workload, not beside it
The design premise is simple: any control that runs beside a workload — a sidecar, an agent, a userspace proxy — is something that the workload’s own compromise can see, stall, or step around. A control that runs below it, in the kernel, enforcing at the same instruction as the syscall it’s gating, isn’t in that position. The workload doesn’t get a vote.
Concretely, Nirmata Runtime attaches BPF-LSM programs directly to bprm_check_security and file_open, and cgroup-scoped eBPF programs to the egress path, per pod. Five behaviors, all decided in-kernel, all synchronous with the operation they gate:
- exec — deny process execution by resolved kernel path
- open — deny file access by resolved kernel path
- network — deny egress by destination
- protocol — deny by the application protocol actually spoken on the wire, independent of destination port — a reverse shell dressed up as HTTPS on 443 doesn’t get a pass just because the port looks right
- dns — observe and report on resolution, without blocking, because DNS denial has failure modes worse than the traffic it would stop
A blocked exec returns -EPERM before the process runs. A blocked connection never completes. There’s no window between “decision” and “enforcement” for a compromised workload to race.
One policy language, not a second one to learn
If you already write Kyverno policies for admission, you already know the policy language here: CEL. RuntimePolicy targets workloads the same way — podSelector/namespaceSelector — and expresses allow/deny logic per behavior, including expressions that pull live values from a ConfigMap or an HTTP source on a refresh interval. Admission and runtime become one continuous policy story instead of two unrelated tools with two unrelated syntaxes glued together with tribal knowledge about which one to check first.
Every policy ships in monitor mode by default. You watch what it would deny — including a WouldDeny counterfactual signal — before you ever flip it to enforce. Nothing goes into blocking mode silently.
Besides the language, Nirmata Runtime uses the same reporting APIs i.e., OpenReports and will add fine-grained exceptions similar to Kyverno for a seamless operator experience.
The same guarantee for AI agents in your cluster
A coding agent, an MCP server, an autonomous pipeline step — these are pods too, and the reasoning above applies to them without a special case. But agents raise a specific question that admission control was never built to answer: an agent’s behavior isn’t fully known during deployment time, because part of what it does is generated by a model at runtime. You can review the image. You can’t review the decision the agent hasn’t made yet.
That’s exactly the case for kernel-level enforcement instead of a control living in the agent’s own process or a sidecar next to it. If an agent’s runtime is compromised — a prompt injection, a supply-chain dependency, a tool call that goes somewhere it shouldn’t — the enforcement boundary can’t be argued with, reasoned around, or quietly disabled by the thing running inside it, because it isn’t running in that process at all.
Concretely, with Nirmata Runtime you can, per agent workload:
- Bound which model providers and endpoints it’s allowed to talk to — dns and network behaviors report or block resolution and egress outside an approved list, catching an agent that starts calling a provider nobody reviewed.
- Restrict which binaries and MCP servers it can launch — exec allow-lists close off arbitrary tool invocation to exactly what the workload contract says the agent may run.
- Detect the agent CLIs and SDKs already present in an image you didn’t build with agents in mind, and the model files and MCP configs a workload reads, before those become undeclared surfaces.
Here’s what that looks like as one policy on an agent pod that should only ever reach an internal LLM gateway, and should never read the credentials sitting on its own filesystem:
apiVersion: runtime.nirmata.io/v1alpha1
kind: RuntimePolicy
metadata:
name: agent-gateway-only
spec:
mode: enforce
podSelector:
matchLabels:
nirmata.io/agent: "true"
behaviors:
- network:
allow:
values:
- llm-gateway.ai-platform.svc.cluster.local
- kube-dns.kube-system.svc.cluster.local
deny:
values:
- "*"
- protocol:
allow:
values:
- tls
- dns
deny:
values:
- "*"
- open:
deny:
values:
- "/root/.aws/credentials"
- "/root/.kube/config"
- "/root/.ssh/id_rsa"
- "/var/run/secrets/kubernetes.io/serviceaccount/token"
network and protocol both carry the “*” sentinel, so a connection has to clear both: the gateway’s address over the wrong protocol is denied, and TLS to anywhere else is too. open doesn’t need the sentinel here — these four paths are exactly the ones a compromised agent would reach for to pivot into the cloud account, the cluster, or another host, and naming them is enough. (open needs a BPF-LSM-active node; network/protocol only need cgroup v2 and BPF.)
If a prompt injection gets a tool call to run cat ~/.aws/credentials or talk to some other endpoint entirely, the kernel has already made the decision before the agent’s own process — or whatever compromised it — gets a chance to argue.
This is enforced the same way as any other workload — no agent-specific proxy, no separate control plane, no assumption that the agent’s own guardrails will hold. The worked examples are in examples/shadow-ai/, covering unexpected DNS, AI SDK detection, and MCP server/config discovery end to end.
Observability that says why, not just that
A counter that says “47 blocks today” isn’t monitoring — it’s a number. When Nirmata Runtime denies something, the kernel encodes the decision in the map key, and userspace re-evaluates the compiled policy against the observed target to attribute the exact policy responsible. If an event can’t be attributed, it isn’t dropped silently — it’s counted and logged as a gap (events_dropped_total{reason=”unattributed_kernel_deny”}), because a monitoring blind spot that looks identical to “nothing happened” is worse than a visible error. Findings land as standard Kubernetes OpenReports objects — no proprietary dashboard required to see what your cluster is actually doing.
Raw observation and a readable report are two different things, though, and CEL is what turns one into the other. Observing exec in monitor mode streams every process a pod runs — necessary, because you don’t know what you’re looking for yet, and useless as a report, because most of it is sh, grep, and the rest of a normal container’s argv. monitorFilter is a per-observation CEL predicate that runs before a finding is written, so the noise never reaches the Report at all:
apiVersion: runtime.nirmata.io/v1alpha1
kind: RuntimePolicy
metadata:
name: detect-agent-cli
spec:
mode: monitor
podSelector:
matchLabels:
app: ai-workload
behaviors:
- exec:
deny:
values: ["*"]
monitorFilter:
expressions:
- name: exec-events-only
expression: 'has(event.exec)'
- name: agent-cli-or-inference-server
expression: >-
event.exec.argv.exists(a, a in [
"claude", "codex", "gemini", "aider", "goose", "opencode", "crush",
"cursor-agent", "ollama", "litellm", "text-generation-launcher",
]) ||
event.exec.argv.exists(a,
a.startsWith("@anthropic-ai/") ||
a.startsWith("@openai/") ||
a.startsWith("@google/gemini-cli"))
The first expression is a guard — without it, the second one evaluates against open and network observations too, where event.exec doesn’t exist. The second is the actual question: not “did a binary run” but “was an agent CLI or a local inference server invoked,” matched on the name it was invoked under or the package a launcher fetched, because npx, uvx, and python are just interpreters — the identity is in argv, not in the exec path. One policy, no sidecar parsing logs after the fact, and a Report that already says an agent CLI showed up on a pod nobody built with agents in mind, not that a process ran.
What’s next
Nirmata Runtime for Kyverno can be used standalone but works best with Kyverno for configuration-level policy enforcement and AI Gateways like AIControls for multi-layered governance for AI agents and LLM-enabled applications. Stay tuned for more announcements on the integrated experience.
Check out the repo for examples and let us know your feedback:
https://github.com/nirmata/runtime
We’re not done. Check the roadmap, and let us know what you would like to see. And, if you like the project make sure you give it a ⭐ to show your support, and to receive updates.

