π Kyverno-style CEL policies for eBPF runtime enforcement.
Nirmata Runtime monitors and enforces runtime behaviors with Kyverno-style CEL policies using eBPF. It provides a per-node DaemonSet that attaches eBPF programs to the pods selected by a cluster-scoped RuntimePolicy.
The RuntimePolicy governs five kinds of workload behavior:
- the files a process opens,
- the binaries it executes,
- the destinations it sends traffic to,
- the application protocols it speaks, and
- the DNS names it resolves.
Decisions are made in the kernel, so a denied operation never completes.
Like Kyverno, everything in Nirmata Runtime is Kubernetes-native: policies are custom resources defined in this project and support CEL (Common Expressions Language); findings are written as OpenReports Report objects in the offending pod's namespace, per-node state and conditions live in the policy's status, and counters are exposed to Prometheus.
As applications become AI-enabled, there is an immediate need to easily monitor and enforce runtime behaviors. Nirmata Runtime complements admission controllers, native RBAC and network policies, and AI gateways by providing an easy to use runtime tool to restrict AI workloads to a bounded set of behaviors and detect rogue agents.
-
Admission Controllers checks the spec; Runtime checks the behavior. Kyverno at admission validates what a pod declares before it starts. Nirmata Runtime enforces what the running process actually does β the files it opens, the binaries it execs, the addresses it contacts β after admission has already said yes.
-
What your CNI can't tell you. NetworkPolicy decides who may reach whom; it does not know that
port: 443might carry SSH, an h2c tunnel, or a custom protocol instead of TLS.protocolclassifies each flow from its first data segment, independent of the declared port, and joinsexec,open, andnetworkin one policy object evaluated by one daemon β a cross-domain assertion no CNI expresses, with each finding attributed to the pod and container it came from, including the process forexec/openfindings. Connectivity, identity-based policy, FQDN egress, ingress, and encryption stay with the CNI; see why a runtime layer for the full split. -
Blocks, not just alerts. Runtime detection tells you a sensitive file was read.
mode: enforcereturns-EPERMfrom a BPF-LSM hook, so the read never happens.mode: monitorgives you the detection workflow first, with the same policy object. -
One small CRD, Kyverno CEL, deliberately narrow. Nirmata Runtime covers five behaviors with one
RuntimePolicyCRD, allow and deny lists, and the same CEL libraries used across Kyverno β including deny lists fetched from ConfigMaps or HTTP feeds at evaluation time.
-
Five behaviors:
open(file paths),exec(binary paths),network(addresses, CIDRs, domain names, and cluster Service names), andprotocol(the application protocol a flow speaks) can be enforced or observed, in any combination in one policy. A fifth,dns(the names a workload resolves), only ever observes: pairing it withmode: enforceis refused, because blocking a destination named by domain is what anetworkbehavior does. -
Enforce or monitor:
spec.modeis per policy, so one policy can block a workload while another reports on it. -
Readable findings: a
monitorFilteris a per-observation CEL predicate deciding which monitor-mode findings are reported, so a discovery policy can watch broadly and still produce a Report someone will read. -
Default deny with allow-lists:
deny.values: ["*"]flips a behavior to deny-all-except-allowed. -
CEL-powered rules: literal
values, CELexpression, reusablespec.variables, and the Kyvernoresource,http, andjsonlibraries alongside the Kubernetes CEL libraries. -
Kubernetes-native output: OpenReports
Reportobjects, per-nodestatus.nodesshards withAppliedandTargetsValidconditions, and Prometheus counters. -
Selector scoping:
podSelectorandnamespaceSelectorlabel selectors, cluster-wide; omitting either selects everything, and anenforce-mode policy must set one of them. -
Periodic re-evaluation:
evaluationIntervalre-runs the policy's expressions, so an externally sourced deny list stays current without editing the policy.
π¨ WARNING: This project is pre-1.0 and the API is v1alpha1. Here are some known limitations:
- Egress is keyed on IPv4 destination addresses. A domain name or cluster Service name is accepted as a value and resolved to addresses; an IPv6 literal is not.
- File
openand processexecenforcement require a kernel booted with BPF-LSM active:bpfmust appear in/sys/kernel/security/lsm(set with thelsm=kernel boot parameter). Stock distributions and hosted CI runners are typically not booted with it. network,protocol,open, andexecobservations come from eBPF counters that the daemon drains on a poll interval rather than from a stream of events, so a finding can lag the behavior and carries counts rather than ordering. Adnsquestion is streamed as it happens.- Exceptions are not yet supported.
-
A Kubernetes cluster on Linux nodes, plus
kubectl,helm, andgit. A stock kind cluster works. -
Network egress enforcement and observation require only a cgroup v2 host and BPF support; a stock kind cluster on a Linux host qualifies. That, plus egress from the cluster to the address each sample below probes, is all they need.
-
File
openand processexecenforcement need a node booted with BPF-LSM active. Whether a managed distribution gives you one is listed in platforms.
helm install kyverno-runtime oci://ghcr.io/nirmata/charts/kyverno-runtime \
--namespace kyverno-runtime --create-namespacekubectl get pods -n kyverno-runtimeTo build from source instead, or to install a daemon image you built yourself, see installation.
Nothing to clone and no server to run. Start a client:
kubectl run egress-client --image=busybox:1.36 --labels=app=egress-client \
--restart=Never --command -- sleep 3600
kubectl wait --for=condition=Ready pod/egress-client --timeout=90s8.8.8.8 is Google Public DNS. It is used here only because it is a recognizable address
that answers from anywhere with egress, so the sample needs nothing of your own running β
blocking a public resolver is a demonstration, not a recommendation. The client can reach
it:
kubectl exec egress-client -- timeout 5 nslookup example.com 8.8.8.8Deny that one address:
kubectl apply -f - <<'EOF'
apiVersion: runtime.nirmata.io/v1alpha1
kind: RuntimePolicy
metadata:
name: block-address-sample
spec:
mode: enforce
podSelector:
matchLabels:
app: egress-client
behaviors:
- network:
deny:
values:
- "8.8.8.8"
EOFThe same query now times out, because the packet is dropped in the kernel β while everything else the pod does, cluster DNS included, keeps working:
kubectl exec egress-client -- timeout 5 nslookup example.com 8.8.8.8 # times out
kubectl exec egress-client -- nslookup kubernetes.default # still resolvesNo CNI, iptables rule, or sidecar is involved, and nothing about the pod spec changed. A real policy names the destinations that matter to the workload, and can name them as domain names or cluster Service names rather than literal addresses β see examples.
kubectl delete rpol block-address-sample
kubectl delete pod egress-clientEnforcement is half of it. The other half is finding out what a workload does that nobody
declared β a model provider it was never approved to call, an SDK nobody reviewed, an MCP
server not in the image contract. A dns behavior declares the names a workload is
expected to resolve and reports the rest, and it needs only the cgroup v2 host the sample
above already used:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: dns-client
labels:
app: dns-client
spec:
containers:
- name: client
image: busybox:1.36
command: ["sh", "-c", "sleep 900"]
EOF
kubectl wait --for=condition=Ready pod/dns-client --timeout=90skubectl apply -f - <<'EOF'
apiVersion: runtime.nirmata.io/v1alpha1
kind: RuntimePolicy
metadata:
name: report-unexpected-dns
spec:
mode: monitor
podSelector:
matchLabels:
app: dns-client
behaviors:
- dns:
allow:
values:
- api.openai.com
- api.anthropic.com
- "*.openai.azure.com"
EOFResolve one approved provider and one that is not. The trailing dot makes each name
absolute, so exactly one question goes on the wire rather than one per search domain in
the pod's /etc/resolv.conf, and whether an answer comes back is irrelevant β the question
is the observation:
kubectl exec dns-client -- nslookup api.openai.com. >/dev/null 2>&1
kubectl exec dns-client -- nslookup api.mistral.ai. >/dev/null 2>&1Only the undeclared name is reported, attributed to the pod that asked for it. Questions reach userspace as they happen and findings are flushed every 10 seconds, so allow about that long:
kubectl get report kyverno-runtime-dns-client \
-o jsonpath='{range .results[?(@.rule=="dns")]}{.properties.dnsName}{"\n"}{end}'api.mistral.ai appears; api.openai.com does not.
kubectl delete rpol report-unexpected-dns
kubectl delete pod dns-clientThe question crosses the wire in cleartext, so this works without touching the workload, its TLS, or its trust store. What was said over that connection is not knowable here β see detecting shadow AI for the signals that are, and the ones that are not. The manifests above, with the full walkthrough, are in examples/shadow-ai/report-unexpected-dns/.
Full walkthroughs: network egress, including how to flip a
policy to monitor and read the resulting Report, plus
file reads and
process exec, which need a BPF-LSM kernel.
- Quickstart - kind cluster to a kernel-enforced egress block in under five minutes; runs on any Linux host
- Why a runtime layer - what a gateway, a TLS proxy, admission control, and a CNI each cannot see, and what this leaves to them
- Concepts - how enforcement works, allow and deny semantics, modes, scoping, and what monitor mode sees
- Installation - platform requirements, Helm chart values, daemon flags
- Platforms - BPF-LSM support across EKS, GKE, AKS, and Bottlerocket, and what works without it
- Examples - the scenario catalog, grouped by feature
- Detecting shadow AI - the providers a workload resolves, the SDKs and model files it reads, the agent CLIs and MCP servers it launches, and what stays inside TLS
- Troubleshooting - why nothing is blocked, missing Reports, rejected targets
- Reference - RuntimePolicy, CEL, Metrics
- Development guide - building, testing, generated artifacts, CI
- Design document - architecture and design decisions
-
π Examples: every scenario under examples/ is a self-contained, CI-validated walkthrough
-
π Spec reference: RuntimePolicy for every field, condition, and documented limit
-
π» CEL: CEL reference for the expression contract and the available libraries
Contributions are welcome. Start with CONTRIBUTING.md for how to
propose a change and get it reviewed, and
docs/dev/DEVELOPMENT.md for build and test mechanics, the
test layout, and how generated artifacts are regenerated. Sign your commits
(git commit -s). Bugs and feature requests go to
GitHub issues.
Security vulnerabilities do not go to the issue tracker. Report them privately by the process in SECURITY.md.
Nirmata Runtime is licensed under the Apache License 2.0.
- Common Expression Language (CEL)
- Kubernetes CEL libraries
- Kyverno CEL libraries
- OpenReports
- BPF LSM (Linux kernel documentation)
Built with β€οΈ by the Nirmata team