root/security-operations/ebpf-threat-detection-for-container-escapes
08/07/2026|5 min read|

eBPF Threat Detection for Container Escapes

Technical breakdown of eBPF threat detection using LSM hooks, ring buffers, and CO-RE to catch container escapes before they complete.
eBPF Threat Detection for Container Escapes

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 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.

eBPF threat detection

#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:

bash
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:

c
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:

yaml
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: Sigkill

This 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.

eBPF threat detection

#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:

json
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_uring submission 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_reserve failures 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 Sigkill actions 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_BPF and CAP_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.

Marcus Thorne

Written By

Marcus Thorne

Marcus Thorne is a pragmatic software architect focused on highly concurrent, distributed transactional systems. With a robust background in designing eventual consistency models and event-driven microservices in Rust and Go, he has successfully transitioned monolithic architectures to event-sourced paradigms at scale. His technical philosophy prioritises mechanical sympathy, performance profiling, and rigorous domain-driven design.