Standard container isolation degrades the moment a workload requests elevated capabilities, mounts a hostPath volume, or triggers a kernel CVE. Seccomp profiles are static, AppArmor policies are compiled at build time, and auditd operates on a slow, buffered pipeline that cannot correlate a syscall with the cgroup or mount namespace that issued it. By the time a SIEM ingests the audit log, the attacker has already pivoted from the container runtime into the host PID namespace. This is the exact gap that eBPF runtime security was built to close: kernel-level, in-line observability with sub-microsecond overhead and full namespace attribution, without recompiling the kernel or restarting the workload.
This article breaks down the architecture required to build a production-grade detection pipeline for container escapes using eBPF LSM (Linux Security Module) hooks, covering the verifier constraints, the userspace aggregation layer, and the failure modes that catch teams out when they move from a proof-of-concept to a fleet-wide rollout.
#The Problem: Static Policy vs Dynamic Kernel State
Container escape detection has traditionally relied on one of three mechanisms: seccomp-bpf syscall filtering, AppArmor/SELinux mandatory access control, or userspace audit correlation (auditd, Falco’s original kernel module). Each has a structural weakness:
eBPF Intrusion Detection for K8s Lateral Movement
- Seccomp-bpf filters syscalls by number and argument pattern but has no concept of container identity, cgroup path, or mount namespace. It can block
unshare()outright, but it cannot conditionally allow it based on runtime context. - AppArmor/SELinux profiles are compiled per-image and rarely updated post-deployment. A profile permissive enough to run a legacy application is permissive enough to permit privilege escalation via a writable
/proc/sysmount. - auditd generates high-cardinality log volume with no in-kernel filtering, forcing expensive userspace correlation and introducing detection latency measured in seconds, not microseconds.
None of these solve the core requirement: correlating a kernel-level security-relevant event (a namespace change, a capability escalation, a suspicious ptrace attach) with the exact container, pod, and process tree that generated it, in real time, at the point of execution rather than after the fact.
#Architectural Breakdown: LSM Hooks and the BPF Verifier
The architecture underpinning modern eBPF runtime security rests on the BPF_PROG_TYPE_LSM program type, introduced in Linux 5.7. Unlike kprobes, which attach opportunistically to arbitrary kernel functions and are subject to ABI drift across kernel versions, LSM hooks attach to stable, security-relevant chokepoints defined in security/security.c: bprm_check_security, task_kill, file_open, sb_mount, and capable among them.

Every eBPF LSM program must pass the in-kernel verifier before load. The verifier performs static analysis to guarantee bounded loops, no unchecked pointer arithmetic, and no possibility of the program hanging or crashing the kernel. This is the trade-off that makes eBPF runtime security viable at scale: the kernel guarantees the safety of third-party code without a context switch to userspace on the hot path.
Data flows out of the kernel via a BPF ring buffer (BPF_MAP_TYPE_RINGBUF), which replaced the older perf buffer implementation to eliminate per-CPU memory waste and reduce event-copy overhead. The userspace agent (commonly built on libbpf and BPF CO-RE) polls this buffer, enriches each event with container metadata pulled from /proc/[pid]/cgroup and the cgroup ID cached in a BPF hash map, and forwards structured events to a detection engine.
#Why eBPF Runtime Security Outperforms Kernel Modules
Legacy kernel-module-based detection (the original Falco driver, for instance) requires out-of-tree compilation against the exact running kernel headers, which is a maintenance burden across heterogeneous fleets. eBPF programs compiled with CO-RE relocations read kernel struct offsets from embedded BTF (BPF Type Format) data at load time, meaning a single compiled artifact runs across kernel 5.8 through 6.x without recompilation. This portability is a primary reason eBPF runtime security has displaced kernel-module approaches in production observability stacks such as Cilium Tetragon and Falco’s modern eBPF driver.
#Implementation Logic: From Hook to Alert
Building the detection pipeline follows a consistent sequence regardless of the specific tool used:
- Step 1 — Hook selection: Attach to
security_bprm_checkfor exec-time detection,security_sb_mountfor host filesystem remounts, andsecurity_ptrace_access_checkfor process-injection attempts. - Step 2 — Context capture: Inside the BPF program, read
task_struct->nsproxy->pid_ns_for_childrento derive the PID namespace inode, andbpf_get_current_cgroup_id()to bind the event to a specific container. - Step 3 — In-kernel filtering: Apply an allow-list lookup against a BPF hash map keyed by binary path hash, discarding benign events before they ever reach the ring buffer. This is what keeps CPU overhead below the commonly cited 2-3% threshold under sustained syscall load.
- Step 4 — Userspace correlation: The agent joins the cgroup ID against the container runtime’s metadata store (containerd’s CRI API or the Kubernetes API server) to resolve pod name, namespace, and image digest.
- Step 5 — Policy evaluation: Events are matched against a declarative ruleset (a TracingPolicy or Falco rule) and either logged, alerted, or used to trigger an in-kernel
bpf_send_signal()to kill the offending process synchronously.
#Enforcing In-Kernel Kill Decisions
The most advanced deployments of eBPF runtime security do not stop at detection; they enforce a kill decision inside the LSM hook itself, returning a non-zero value from the BPF program to deny the syscall before it completes. This closes the race-condition window that plagues userspace-only detection, where an attacker’s escape syscall has already succeeded by the time an alert reaches a human or an automated responder.

#Code and Configuration Examples
#1. Minimal LSM Hook in C (libbpf CO-RE)
1SEC("lsm/bprm_check_security")
2int BPF_PROG(detect_exec, struct linux_binprm *bprm)
3{
4 u64 cgroup_id = bpf_get_current_cgroup_id();
5 const char *filename = BPF_CORE_READ(bprm, filename);
6
7 struct exec_event *e;
8 e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
9 if (!e)
10 return 0;
11
12 e->cgroup_id = cgroup_id;
13 e->pid = bpf_get_current_pid_tgid() >> 32;
14 bpf_probe_read_str(&e->filename, sizeof(e->filename), filename);
15
16 bpf_ringbuf_submit(e, 0);
17 return 0; // 0 = allow, non-zero = deny the exec
18}#2. Cilium Tetragon TracingPolicy for Mount Namespace Escapes
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4 name: detect-host-mount-escape
5spec:
6 kprobes:
7 - call: security_sb_mount
8 syscall: false
9 args:
10 - index: 0
11 type: "string"
12 - index: 1
13 type: "path"
14 selectors:
15 - matchArgs:
16 - index: 1
17 operator: "Prefix"
18 values:
19 - "/host"
20 - "/proc/1/root"
21 matchActions:
22 - action: Sigkill#3. Verifying Load Success and Ring Buffer Health with bpftool
1$ bpftool prog show name detect_exec
242: lsm name detect_exec tag 3a9f2b7c1d0e gpl
3 loaded_at 2024-03-11T09:14:02+0000 uid 0
4 xlated 512B jited 320B memlock 4096B map_ids 7,8
5
6$ bpftool map dump id 7 | head -n 5
7key: 00 00 00 00 00 00 00 00 value: 6f 6e 65 5f 62 69 6e...
8
9# Confirm no ring buffer drops under load
10$ cat /proc/sys/kernel/bpf_stats_enabled
111
12$ bpftool prog show name detect_exec --json | jq '.run_time_ns'
131843221#Failure Modes and Edge Cases
Deploying eBPF runtime security fleet-wide surfaces several failure classes that rarely appear in single-node testing:
- Verifier rejection on kernel upgrade: A program that loads cleanly on 6.1 may be rejected on 5.10 due to differing complexity limits (the verifier’s instruction-count ceiling was raised progressively). Pin CO-RE builds to your minimum supported kernel and test against it explicitly in CI.
- Ring buffer overflow under syscall storms: A fork-bomb or a legitimate CI runner generating thousands of exec events per second can exhaust the ring buffer faster than userspace can drain it, causing silent event loss. Size the buffer generously (8-16MB per CPU) and monitor
bpf_ringbuf_query()drop counters directly rather than relying on downstream alert gaps. - TOCTOU races in file-based hooks: A hook on
file_openthat reads a file’s contents for signature matching after the open call completes can be beaten by a symlink swap between the check and the read. Mitigate by hashing atbprm_check_securitytime, before the kernel maps the executable into memory. - Cgroup ID reuse: Kubernetes recycles cgroup IDs aggressively on pod churn. Without a monotonic pod UID join key, high-velocity clusters will misattribute events to a terminated pod’s replacement, producing false attribution in forensic timelines.
- Kernel lockdown mode conflicts: Nodes running in
lockdown=integritymode restrict certain BPF helper functions (notably raw kernel memory writes), which can silently disable enforcement-mode programs while detection-only programs continue to function, giving a false sense of protection.
#Scaling and Security Trade-offs
Rolling eBPF runtime security across a multi-tenant fleet requires explicit trade-off decisions rather than default configuration. Teams building this into their broader architectural patterns for platform security should evaluate the following axes before committing to enforcement mode cluster-wide:
- Detection-only vs enforcement: Detection-only deployments carry near-zero risk of application disruption but leave the TOCTOU window open; enforcement mode closes that window but risks killing legitimate processes on rule misconfiguration.
- Per-core overhead: Well-optimised eBPF runtime security pipelines with in-kernel filtering sustain 2-3% CPU overhead at 50k events/sec per node; unfiltered pipelines that ship every syscall to userspace can exceed 15% under load.
- Kernel version fragmentation: Fleets spanning kernel 4.x (pre-LSM-BPF) require a fallback to kprobe-based instrumentation, which lacks the ABI stability guarantees of the LSM hook API and increases maintenance surface.
- Alert fidelity vs volume: Broad hook coverage (every
capable()call, for example) generates high alert volume with a poor signal-to-noise ratio; narrowing to security-relevant capability checks (CAP_SYS_ADMIN,CAP_SYS_PTRACE) materially reduces false positives without losing coverage of known escape techniques. - Privileged agent exposure: The userspace agent itself typically runs with
CAP_BPFandCAP_PERFMON, making it a high-value target; it must be treated as part of the trusted computing base and isolated in its own restrictive seccomp profile, distinct from the workloads it monitors.
The architectural decision ultimately hinges on the organisation’s tolerance for enforcement-induced disruption against its exposure to kernel-level compromise. Reference the upstream Linux kernel BPF documentation when validating hook availability and verifier constraints against your specific kernel baseline before committing a policy to production enforcement.






