Traditional runtime security tooling — auditd, seccomp-notify daemons, and userspace syscall wrappers — cannot keep pace with modern container escape techniques. The eBPF threat detection model solves a specific bottleneck: kernel-level visibility into process execution, namespace manipulation, and capability escalation without the 15-30% syscall overhead penalty typically associated with ptrace-based introspection. When an attacker pivots from a compromised container to the host via a mounted /proc filesystem, a misconfigured hostPath volume, or a stale CAP_SYS_ADMIN capability, the detection window is measured in milliseconds. Auditd’s rule-matching engine, operating on a netlink socket fed by the kernel audit subsystem, simply cannot filter and correlate at that velocity under sustained load.
This article covers the architecture for building a production-grade eBPF threat detection pipeline using Linux Security Module (LSM) hooks, ring buffers, and CO-RE (Compile Once, Run Everywhere) portability. It assumes familiarity with kernel tracing primitives and container runtime internals (runc, containerd shim v2).
#The Architectural Bottleneck: Why Syscall Auditing Fails at Scale
Auditd hooks into the kernel via the audit_context struct attached to every task, generating a record for every matched syscall. On a busy Kubernetes node running 200+ pods, this produces an unmanageable volume of netlink traffic — often exceeding 40,000 events per second during build pipelines or package installs. The userspace daemon becomes the bottleneck, backpressure builds in the kernel audit queue, and under sustained load the kernel will either drop records or, if audit_backlog_wait_time is misconfigured, stall the calling process entirely.
eBPF Runtime Security for Container Escapes
eBPF sidesteps this by executing verified bytecode directly in kernel context, attached to specific hook points, with filtering logic applied before data crosses the kernel-userspace boundary. This is the core architectural shift: instead of streaming raw events to userspace for filtering, filtering happens in-kernel, and only high-signal events traverse the ring buffer.
#LSM Hooks vs Kprobes vs Tracepoints
Three attachment mechanisms are commonly conflated in eBPF threat detection designs, but they carry materially different guarantees:
- Kprobes/kretprobes — attach to arbitrary kernel function entry/exit points. Fragile across kernel versions because function signatures and inlining behaviour change between releases; requires CO-RE relocations to remain portable.
- Tracepoints — stable ABI, defined by the kernel maintainers (e.g.
sys_enter_execve). More reliable than kprobes but lack the ability to deny an action — they are observational only. - LSM hooks (via
BPF_PROG_TYPE_LSM) — attach to the same interception points used by SELinux and AppArmor (bprm_check_security,task_alloc,sb_mount,socket_connect). Crucially, LSM programs can return a non-zero value to block the operation synchronously, not just log it after the fact.
For container escape detection specifically, LSM hooks on security_bprm_check, security_sb_mount, and security_ptrace_access_check give you both detection and, if required, enforcement — a materially stronger posture than tracepoint-only pipelines like classic Falco.
#Implementation Logic: Building the Detection Pipeline
The pipeline breaks into four stages: hook attachment, in-kernel filtering, ring buffer transport, and userspace correlation. This mirrors standard architectural patterns for event-driven systems, just relocated partially into kernel space.

#Step 1 — Attach the LSM Program
The kernel must be built with CONFIG_BPF_LSM=y and CONFIG_LSM must list bpf in the active LSM stack. Verify with:
1cat /sys/kernel/security/lsm
2# expected output: capability,landlock,yama,bpf,apparmor
3
4uname -r
5# 5.10+ required for stable LSM BPF program type#Step 2 — Write the Kernel-Side Filter
The following BPF C snippet hooks security_bprm_check to flag execution of shell binaries from within a container namespace that does not match a known base image path — a common signature of a post-escape reverse shell:
1SEC("lsm/bprm_check_security")
2int BPF_PROG(detect_shell_exec, struct linux_binprm *bprm)
3{
4 struct task_struct *task = (struct task_struct *)bpf_get_current_task();
5 u32 pid_ns_inum = BPF_CORE_READ(task, nsproxy, pid_ns_for_children, ns.inum);
6
7 const char *filename = BPF_CORE_READ(bprm, filename);
8 char comm[16];
9 bpf_get_current_comm(&comm, sizeof(comm));
10
11 if (is_shell_binary(filename) && pid_ns_inum != HOST_PID_NS) {
12 struct escape_event *e;
13 e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
14 if (!e)
15 return 0;
16
17 e->pid_ns = pid_ns_inum;
18 e->pid = bpf_get_current_pid_tgid() >> 32;
19 bpf_probe_read_kernel_str(&e->filename, sizeof(e->filename), filename);
20 bpf_ringbuf_submit(e, 0);
21 }
22 return 0;
23}Note the use of BPF_CORE_READ rather than direct struct dereference — this generates CO-RE relocations resolved at load time against the target kernel’s BTF (BPF Type Format), avoiding the need to recompile per kernel version. This is the single most important portability decision in any eBPF threat detection deployment targeting heterogeneous fleets.
#Step 3 — Ring Buffer Transport, Not Perf Buffer
Legacy tooling (bcc-based Falco modules) used the perf ring buffer, which suffers from per-CPU buffer fragmentation and lost-event windows under bursty load. The newer BPF_MAP_TYPE_RINGBUF, available since kernel 5.8, provides a single shared buffer with proper backpressure semantics and roughly 30-40% lower CPU overhead on the consumer side under high event throughput. Any current eBPF threat detection pipeline should standardise on ringbuf.
#Step 4 — Policy Definition at the Orchestration Layer
Rather than hand-rolling BPF C for every signature, most production deployments sit on top of Cilium Tetragon or Falco’s eBPF probe, expressing detection logic declaratively. A Tetragon TracingPolicy targeting namespace escape via setns() abuse looks like this:
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4 name: container-escape-setns
5spec:
6 kprobes:
7 - call: "__x64_sys_setns"
8 syscall: true
9 args:
10 - index: 0
11 type: "fd"
12 - index: 1
13 type: "int"
14 selectors:
15 - matchArgs:
16 - index: 1
17 operator: "Equal"
18 values:
19 - "1073741824" # CLONE_NEWNS
20 matchActions:
21 - action: Post
22 rateLimit: "1/m"
23 - action: SigkillThis policy fires on any attempt to join the host mount namespace and, in enforcing mode, terminates the offending process with Sigkill before the escape completes — converting detection into prevention at the kernel boundary.

#Correlation Layer: From Kernel Events to Actionable Alerts
Raw ring buffer events lack context — a PID and namespace inode number mean nothing to a SOC analyst. The userspace consumer must enrich events against the container runtime’s metadata store (containerd’s events API or the CRI socket) to resolve namespace inode to pod name, image digest, and node identity. A typical enriched output, forwarded to a SIEM via OpenTelemetry, resembles:
1{
2 "event_type": "eBPF_threat_detection",
3 "hook": "security_bprm_check",
4 "pid_ns_inode": 4026532871,
5 "pod": "payment-worker-7f9c4",
6 "namespace": "prod-billing",
7 "binary": "/bin/dash",
8 "parent_process": "containerd-shim",
9 "severity": "critical",
10 "action_taken": "blocked",
11 "timestamp": "2024-05-14T02:11:43.881Z"
12}This enrichment step is where most home-grown eBPF threat detection builds fail in practice — the kernel side is comparatively easy; maintaining a low-latency, consistent mapping between namespace inodes and workload identity across pod churn is the harder distributed-systems problem.
#Failure Modes and Edge Cases
Several failure classes are specific to this architecture and are frequently omitted from vendor documentation:
- Verifier rejection under kernel upgrade. The BPF verifier’s complexity limits (historically capped around one million instructions, now soft-limited by state pruning) can reject a program on one kernel minor version that loaded cleanly on another. CO-RE mitigates struct layout drift but does not guarantee verifier acceptance across major version jumps (e.g. 5.15 to 6.1 changed several helper function signatures).
- io_uring bypass. Syscall-based hooks (kprobes on syscall entry) can be circumvented by workloads using
io_uringsubmission queues, which route I/O operations through a different kernel path. LSM hooks on the underlying security functions (rather than the syscall wrapper) are more resilient here, but coverage is not universal across all LSM hook points as of kernel 6.6. - Ring buffer saturation. Under a syscall flood (e.g. a fork bomb inside a compromised container), even filtered event volume can exceed consumer drain rate. Undersized ring buffers silently drop events —
bpf_ringbuf_reservefailures are not surfaced to the security layer unless explicitly instrumented with a drop counter map. - False positives from legitimate namespace operations. CI runners, sandboxed build tools (e.g. Buildah, unshare-based rootless builds), and service meshes performing network namespace manipulation for sidecar injection will trigger the same LSM hooks as genuine escapes. Policies require environment-aware allow-listing keyed on parent process lineage, not just syscall arguments.
- BTF unavailability on minimal/embedded kernels. Stripped-down distributions (some hardened container-optimised OS images) ship without embedded BTF data, breaking CO-RE relocation entirely unless an external BTF blob is supplied at load time via
btf_custom_path.
#Scaling and Security Trade-offs
Deploying eBPF threat detection fleet-wide surfaces trade-offs that differ meaningfully from userspace agent-based tooling:
- Enforcement vs observability — LSM hooks with
Sigkillactions provide real prevention but introduce availability risk from false positives; tracepoint-only deployments are safer operationally but detection-only, with response latency dependent on downstream SOC tooling. - Per-node CPU overhead — well-optimised LSM/ringbuf pipelines typically add 1-3% CPU overhead per node under normal load versus 8-12% for auditd with an equivalent rule set at high syscall rates.
- Kernel version fragmentation — fleets spanning kernel 4.18 (some enterprise RHEL derivatives) through 6.x require either dual code paths or dropping LSM BPF support entirely on legacy nodes, falling back to kprobe-only coverage with reduced enforcement capability.
- Event pipeline throughput — centralised aggregation (Kafka, or a managed equivalent) must be sized for worst-case burst, not average load; a single fork bomb across a 500-node cluster can generate sustained multi-million-event-per-minute spikes even after in-kernel filtering.
- Privilege footprint of the loader — loading BPF LSM programs requires
CAP_BPFandCAP_SYS_ADMIN(pre-5.8) or a narrower capability set on newer kernels; the loader daemon itself becomes a high-value target and should run with a minimal, audited privilege boundary, ideally itself monitored by a separate, simpler detection mechanism to avoid a single point of blind trust.
Reference implementations and the current BPF LSM helper surface are documented in the kernel BPF subsystem documentation, which should be treated as the canonical source given how frequently helper semantics shift between releases.






