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

eBPF Runtime Security for Container Escapes

A technical breakdown of eBPF runtime security using LSM hooks, ring buffers, and Tetragon policies to detect and block container escapes in real time.
eBPF Runtime Security for Container Escapes

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:

  • 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/sys mount.
  • 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.

eBPF runtime security

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_check for exec-time detection, security_sb_mount for host filesystem remounts, and security_ptrace_access_check for process-injection attempts.
  • Step 2 — Context capture: Inside the BPF program, read task_struct->nsproxy->pid_ns_for_children to derive the PID namespace inode, and bpf_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.

eBPF runtime security

#Code and Configuration Examples

#1. Minimal LSM Hook in C (libbpf CO-RE)

c
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

yaml
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

bash
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_open that 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 at bprm_check_security time, 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=integrity mode 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_BPF and CAP_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.

Eleanor Hayes

Written By

Eleanor Hayes

Dr Eleanor Hayes is a veteran cryptography researcher and enterprise security architect specialising in zero-trust network implementations. Having spent a decade securing critical national infrastructure, she now designs identity-aware proxy perimeters and mutual TLS topologies for highly distributed environments. She holds multiple GIAC certifications and regularly consults on mitigating advanced persistent threats within microservice ecosystems.