eBPF runtime security with Cilium¶
Scope: Cilium (cilium/cilium, Apache-2.0), the eBPF-based networking, security, and observability layer for Kubernetes, viewed specifically as the runtime enforcement plane under agentic workloads. This page covers identity-based network policy (how Cilium decides connectivity from labels rather than IPs), default-deny semantics, L3/L4/L7 and DNS-aware egress control, and Hubble flow observability, then why an agent that runs untrusted tools needs this layer and how to configure it. It is the containment entry in the agentic cybersecurity and SysOps index, the enforcement counterpart to the offensive tools there, and it extends the cluster's Kubernetes networking and network drivers.
Examined at commit
8c0423e9of thecilium/ciliumdefault branch (version1.21.0-dev); the latest stable release line at the time of writing is 1.20.x, so pin to a stable tag for any deployment. Cilium is a CNCF Graduated project. The identity-based policy decision below is executed and asserted as standalone Python, modeled onexamples/policies/l3/simple/l3.yamlandexamples/policies/l4/l4.yaml. No cluster was provisioned: Cilium needs a Linux kernel with eBPF and a running Kubernetes cluster, so throughput and enforcement claims are read from the project's docs and code, not benchmarked here.
What it is¶
Cilium is a CNI (container network interface) plugin and a security and observability platform built on eBPF, the in-kernel programmable-datapath technology. Instead of matching traffic on IP addresses, Cilium assigns each endpoint (a pod, roughly) a numeric identity derived from its labels, and enforces policy on those identities in the kernel datapath. Three capabilities matter for containment:
- Identity-based network policy.
CiliumNetworkPolicyselects endpoints with label selectors (matchLabels,matchExpressions) and allows or denies ingress and egress by the peer's identity, at L3 (identity), L4 (port and protocol), and L7 (HTTP methods and paths, DNS names, Kafka topics). - Default-deny, per direction. An endpoint is allow-all until a policy selects it in a direction; once any policy applies, that direction becomes deny-by-default and needs an explicit allow. This is the property that turns policy into a real boundary.
- Hubble observability. Cilium's observability layer gives per-flow visibility (which identity talked to which, on what port, allowed or dropped), which is the audit trail for an agent's actual network behavior.
Because enforcement is in eBPF, it runs in the kernel datapath without a sidecar proxy for L3/L4, and it stays valid as pods churn (identity follows labels, not the ephemeral IP).
Why use it under agentic workloads¶
An agent that runs untrusted or model-chosen tools (a pentest engine, an SRE agent that executes remediation, a coding agent that runs arbitrary commands) is exactly the workload whose network behavior you cannot fully predict. Cilium is the layer that bounds it:
- Egress control is the containment that matters. The risk is not what reaches the agent, it is where a compromised or over-eager agent can reach: exfiltration, callbacks, lateral movement. Cilium's egress policy, including DNS-aware and L7 rules, restricts an agent to the endpoints and hostnames it legitimately needs.
- Identity survives pod churn. Agent workloads scale and restart; policy keyed on labels keeps enforcing across new pod IPs.
- In-kernel enforcement. No per-connection sidecar for L3/L4 means the containment does not depend on a proxy the agent could bypass.
- Flow-level audit. Hubble records what the agent's tools actually did on the network, which is the evidence you need after an incident and the signal that feeds detection.
- Kubernetes-native. Policy is declarative YAML alongside the workload, so the containment ships with the deployment.
When to use it (and when not)¶
Use it as the network enforcement and observability plane for any Kubernetes cluster running agents that execute untrusted tools, and generally as the CNI for GPU and AI clusters where you want identity-based policy and flow visibility. It is the right layer to box in the pentest and ops agents elsewhere on this index.
Do not treat an L3 label match as trust: as the model below shows, a policy that admits role=frontend admits a compromised frontend too, so L3 identity alone does not contain a peer that is inside the allowed set. Layer L4, L7, and egress restrictions, and pair it with pod-level sandboxing and isolation. Do not expect it to replace application authorization or a policy engine for the agent's actions; it governs the network, not the agent's tool calls (that is the agent policy engine). Do not run the -dev branch in production; pin a stable release.
Architecture¶
flowchart TB
subgraph K8S["Kubernetes node"]
POD["Agent pod (labels -> identity)"] --> EBPF["eBPF datapath"]
EBPF --> POLICY{"CiliumNetworkPolicy: identity + L4 + L7"}
POLICY -->|"allow"| DEST["Permitted endpoints / DNS names"]
POLICY -->|"deny (default)"| DROP["Dropped, logged"]
EBPF --> HUBBLE["Hubble: per-flow observability"]
end
HUBBLE --> AUDIT["Audit + detection"]
Identity-based policy, executed¶
The model below reproduces Cilium's ingress decision: an endpoint is allow-all until a policy selects it, then deny-by-default; a connection is allowed only if a selecting rule admits the peer's identity via matchLabels and matchExpressions. It asserts the frontend-to-backend allow, the stranger deny, the crucial "identity is not trust" case, and a matchExpressions quarantine exclusion.
def selects(selector, labels):
"""True if labels satisfy matchLabels AND every matchExpression."""
for k, v in selector.get("matchLabels", {}).items():
if labels.get(k) != v:
return False
for expr in selector.get("matchExpressions", []):
key, op, vals = expr["key"], expr["operator"], set(expr.get("values", []))
present = key in labels
if op == "In" and (not present or labels[key] not in vals):
return False
if op == "NotIn" and present and labels[key] in vals:
return False
if op == "Exists" and not present:
return False
if op == "DoesNotExist" and present:
return False
return True
def ingress_allowed(target, source, policies):
"""Cilium ingress decision for source -> target under a policy set."""
selecting = [p for p in policies if selects(p["endpointSelector"], target)]
if not selecting:
return True # no policy selects the target -> default allow
for p in selecting:
for rule in p.get("ingress", []):
if any(selects(fe, source) for fe in rule.get("fromEndpoints", [])):
return True
return False # selected but no rule admits this source -> deny-by-default
# Mirror l3/simple: backend accepts ingress only from role=frontend.
l3 = {
"endpointSelector": {"matchLabels": {"role": "backend"}},
"ingress": [{"fromEndpoints": [{"matchLabels": {"role": "frontend"}}]}],
}
backend = {"role": "backend", "app": "api"}
frontend = {"role": "frontend"}
attacker = {"role": "frontend", "compromised": "true"}
stranger = {"role": "batch"}
assert ingress_allowed(backend, stranger, []) is True # 1. no policy -> allow-all
assert ingress_allowed(backend, frontend, [l3]) is True # 2. admitted identity
assert ingress_allowed(backend, stranger, [l3]) is False # non-matching -> denied
print("frontend->backend:", ingress_allowed(backend, frontend, [l3]))
print("batch ->backend:", ingress_allowed(backend, stranger, [l3]))
assert ingress_allowed(backend, attacker, [l3]) is True # 3. identity != trust
print("compromised frontend->backend:", ingress_allowed(backend, attacker, [l3]),
"<- L3 identity != trust")
strict = { # 4. exclude a tier
"endpointSelector": {"matchLabels": {"role": "backend"}},
"ingress": [{"fromEndpoints": [{
"matchLabels": {"role": "frontend"},
"matchExpressions": [{"key": "tier", "operator": "NotIn", "values": ["quarantine"]}],
}]}],
}
q_front = {"role": "frontend", "tier": "quarantine"}
assert ingress_allowed(backend, frontend, [strict]) is True
assert ingress_allowed(backend, q_front, [strict]) is False
print("quarantined frontend->backend:", ingress_allowed(backend, q_front, [strict]))
print("OK: policy flips the endpoint to deny-by-default, then admits by identity, "
"which is why egress/L7 rules are needed to contain a matched-but-compromised peer")
Executed output:
frontend->backend: True
batch ->backend: False
compromised frontend->backend: True <- L3 identity != trust
quarantined frontend->backend: False
OK: policy flips the endpoint to deny-by-default, then admits by identity, which is why egress/L7 rules are needed to contain a matched-but-compromised peer
The third case is the design lesson for agentic workloads. A policy keyed on role=frontend admits any endpoint with that label, including a compromised one. L3 identity is authentication of the label, not trust in the workload. Containing a peer that is inside the allowed set requires L4 (limit it to the one port it needs), L7 (limit it to the specific HTTP paths or DNS names), and egress restriction (so a compromised agent cannot reach out to attacker infrastructure), plus workload-level sandboxing.
How to use it¶
A CiliumNetworkPolicy is declarative YAML. An egress lockdown for an agent pod, restricting it to a named internal service on one port, looks like this reference template (unexecuted, pinned to the cilium.io/v2 API):
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "agent-egress-lockdown"
spec:
endpointSelector:
matchLabels:
app: pentest-agent
egress:
- toEndpoints:
- matchLabels:
app: target-under-test
toPorts:
- ports:
- port: "443"
protocol: TCP
- toFQDNs: # allow one DNS name, deny all other egress by default
- matchName: "api.internal.example.com"
Apply with kubectl apply -f, then watch the flows with hubble observe --pod pentest-agent. Because the endpoint is now selected on egress, everything not explicitly allowed is dropped and logged.
How to develop with it¶
Model policy from the shipped examples under examples/policies (l3/, l4/, l7/, and host/). Start from a default-deny for the direction that matters (egress for containment), then add the minimal allows. Test policy in a kind cluster with Cilium installed before promoting it. The identity model means you design around labels: give agent workloads distinct, stable labels so policy targets them precisely and survives restarts.
How to maintain it¶
Pin a stable Cilium release (1.20.x line as of writing), not the -dev branch examined here, and track the release notes for datapath and policy changes. Cilium spans kernel eBPF, the agent daemonset, and the operator; upgrade them together per the project's upgrade guide, and re-validate policy after an upgrade since selector and L7 semantics occasionally tighten. Keep Hubble retention sized for your audit needs.
How to run it in production¶
Run Cilium as the cluster CNI with Hubble enabled for flow observability. For agentic workloads, make egress default-deny per namespace and grant explicit allows, use toFQDNs for the external hostnames an agent legitimately needs, and apply L7 rules where the agent speaks HTTP to internal services. Feed Hubble flows into your detection pipeline so a policy drop or an unexpected egress attempt is an alert. Cilium is the network plane of a layered posture: combine it with pod sandboxing and isolation, the agent policy engine for tool-call authorization, and risk-tiered approval for state-mutating actions.
Failure modes¶
- L3 identity is not trust. A matched label admits a compromised peer. Layer L4, L7, and egress rules; do not rely on identity alone.
- Policy gaps default to allow. An endpoint no policy selects in a direction is allow-all in that direction. A missing egress policy on an agent pod means unrestricted egress.
- eBPF and kernel coupling. Cilium depends on kernel eBPF features; datapath behavior and available features vary with kernel and Cilium version, so validate on your exact node image.
- Upgrade drift. Agent, operator, and datapath must move together; a partial upgrade can misenforce policy.
- DNS-aware egress caveats.
toFQDNsenforcement depends on Cilium seeing the DNS resolution; unusual resolvers or hardcoded IPs can slip it.
References¶
- Cilium repository, pinned commit
8c0423e9(1.21.0-dev; pin a stable 1.20.x tag for use): https://github.com/cilium/cilium - Policy examples (
examples/policies): https://github.com/cilium/cilium/tree/main/examples/policies - Network policy docs: https://docs.cilium.io/en/stable/security/policy/
- Hubble observability: https://docs.cilium.io/en/stable/observability/hubble/
- Cilium (CNCF Graduated project): https://www.cncf.io/projects/cilium/
Related: Agentic cybersecurity and SysOps index · Kubernetes networking: WireGuard hybrid · Kubernetes network drivers · Agent sandboxing and isolation · Agent policy engine · Risk-tiered human approval gates