BPF (eBPF)
Welcome back to the KubeArmor tutorial! In the previous chapter, we explored the System Monitor, KubeArmor's eyes and ears inside the operating system, responsible for observing runtime events like file accesses, process executions, and network connections. We learned that the System Monitor uses a powerful kernel technology called eBPF to achieve this deep visibility with low overhead.
In this chapter, we'll take a closer look at BPF (Extended Berkeley Packet Filter), or eBPF as it's more commonly known today. This technology isn't just used by the System Monitor; it's also a key enforcer type available to the Runtime Enforcer component in the form of BPF-LSM. Understanding eBPF is crucial to appreciating how KubeArmor works at a fundamental level within the Linux kernel.
What is BPF (eBPF)?
Imagine the Linux kernel as the central operating system managing everything on your computer or server. Traditionally, if you wanted to add new monitoring, security, or networking features deep inside the kernel, you had to write C code, compile it as a kernel module, and load it. This is risky because bugs in kernel modules can crash the entire system.
eBPF provides a safer, more flexible way to extend kernel functionality. Think of it as a miniature, highly efficient virtual machine running inside the kernel. It allows you to write small programs that can be loaded into the kernel and attached to specific "hooks" (points where interesting events happen).
Here's the magic:
Safe: eBPF programs are verified by a kernel component called the "verifier" before they are loaded. The verifier ensures the program won't crash the kernel, hang, or access unauthorized memory.
Performant: eBPF programs run directly in the kernel's execution context when an event hits their hook. They are compiled into native machine code for the processor using a "Just-In-Time" (JIT) compiler, making them very fast.
Flexible: They can be attached to various hooks for monitoring or enforcement, including system calls, network events, tracepoints, and even Linux Security Module (LSM) hooks.
Data Sharing: eBPF programs can interact with user-space programs (like the KubeArmor Daemon) and other eBPF programs using shared data structures called BPF Maps.
Why KubeArmor Uses BPF (eBPF)
KubeArmor needs to operate deep within the operating system to provide effective runtime security for containers and nodes. It needs to:
See Everything: Monitor low-level system calls and kernel events across different container namespaces (Container/Node Identity).
Act Decisively: Enforce security policies by blocking forbidden actions before they can harm the system.
Do it Efficiently: Minimize the performance impact on your applications.
eBPF is the perfect technology for this:
Deep Visibility: By attaching eBPF programs to kernel hooks, KubeArmor's System Monitor gets high-fidelity data about system activities as they happen.
High-Performance Enforcement: When used as a Runtime Enforcer via BPF-LSM, eBPF programs can quickly check policies against events directly within the kernel, blocking actions instantly without the need to switch back and forth between kernel and user space for every decision.
Low Overhead: eBPF's efficiency means it adds minimal latency to system calls compared to older kernel security mechanisms or relying purely on user-space monitoring.
Kernel Safety: KubeArmor can extend kernel behavior for security without the risks associated with traditional kernel modules.
BPF in Action: Monitoring and Enforcement
Let's look at how BPF powers both sides of KubeArmor's runtime protection:
1. BPF for Monitoring (The System Monitor)
As we saw in Chapter 4, the System Monitor observes events. This is primarily done using eBPF.
How it works: Small eBPF programs are attached to kernel hooks related to file, process, network, etc., events. When an event triggers a hook, the eBPF program runs. It collects relevant data (like the path, process ID, Namespace IDs) and writes this data into a special shared memory area called a BPF Ring Buffer.
Getting Data to KubeArmor: The KubeArmor Daemon (KubeArmor Daemon) in user space continuously reads events from this BPF Ring Buffer.
Context: The daemon uses the Namespace IDs from the event data to correlate it with the specific container or node (Container/Node Identity) before processing and sending the alert via the Log Feeder.
Simplified view of monitoring data flow:
This shows the efficient flow: the kernel triggers a BPF program, which quickly logs data to a buffer that KubeArmor reads asynchronously.
Let's revisit a simplified code concept for the BPF monitoring program side (C code compiled to BPF):
Explanation:
struct event
: Defines the structure of the data sent for each event.kubearmor_events
: Defines a BPF map of typeRINGBUF
. This is the channel for kernel -> user space communication.SEC("kprobe/sys_enter_openat")
: Specifies where this program attaches - at the entry of theopenat
system call.bpf_ringbuf_reserve
: Allocates space in the ring buffer for a new event.bpf_ktime_get_ns
,bpf_get_current_task
,bpf_get_current_comm
,bpf_probe_read_str
: BPF helper functions used to get data from the kernel context (timestamp, task info, command name, string from user space).bpf_ringbuf_submit
: Sends the prepared event data to the ring buffer.
On the Go side, KubeArmor's System Monitor uses the cilium/ebpf
library to load this BPF object file and read from the kubearmor_events
map (the ring buffer).
Explanation:
loadMonitorObjects
: Loads the compiled BPF program and map definitions from the.o
file.perf.NewReader(objs.KubearmorEvents, ...)
: Opens a reader for the specific BPF map namedkubearmor_events
defined in the BPF code. This map is configured as a ring buffer.mon.SyscallPerfMap.Read()
: Blocks until an event is available in the ring buffer, then reads the raw bytes sent by the BPF program.The rest of the
readEvents
function (simplified out, but hinted at in Chapter 4 context) involves parsing these bytes back into a struct, looking up the container/node identity, and processing the event.
This demonstrates how BPF allows a low-overhead kernel component (the BPF program writing to the ring buffer) and a user-space component (KubeArmor Daemon reading from the buffer) to communicate efficiently.
2. BPF for Enforcement (BPF-LSM Enforcer)
When KubeArmor is configured to use the BPF-LSM Runtime Enforcer, BPF programs are used not just for monitoring, but for making enforcement decisions in the kernel.
How it works: BPF programs are attached to Linux Security Module (LSM) hooks. These hooks are specifically designed points in the kernel where security decisions are made (e.g., before a file is opened, before a program is executed, before a capability is used).
Policy Rules in BPF Maps: KubeArmor translates its Security Policies into a format optimized for quick lookup and stores these rules in BPF Maps. There might be nested maps where an outer map is keyed by Namespace IDs (Container/Node Identity) and inner maps store rules specific to paths, processes, etc., for that workload.
Decision Making: When an event triggers a BPF-LSM hook, the attached eBPF program runs. It uses the current process's Namespace IDs to look up the relevant policy rules in the BPF maps. Based on the rule found (or the default posture if no specific rule matches), the BPF program returns a value to the kernel indicating whether the action should be allowed (0) or blocked (
-EPERM
, which is kernel speak for "Permission denied").Event Reporting: Even when an action is blocked, the BPF-LSM program (or a separate monitoring BPF program) will often still send an event to the ring buffer so KubeArmor can log the blocked attempt.
Simplified view of BPF-LSM enforcement flow:
This diagram shows the pre-configuration step (KubeArmor loading the program and rules) and then the fast, kernel-internal decision path when an event occurs.
Let's revisit a simplified BPF C code concept for enforcement (part of enforcer.bpf.c):
Explanation:
struct outer_key
: Defines the key structure for the outer map (kubearmor_containers
), usingpid_ns
andmnt_ns
from the process's identity.kubearmor_containers
: A BPF map storing references to other maps (or rule data directly in simpler cases), allowing rules to be organized per container/namespace.SEC("lsm/bprm_check_security")
: Attaches this program to the LSM hook that is called before a new program is executed.BPF_PROG(...)
: Macro defining the BPF program function.get_outer_key
: Helper function to get the Namespace IDs for the current task.bpf_map_lookup_elem(&kubearmor_containers, &okey)
: Looks up the map (or data) associated with the current process's namespace IDs.The core logic involves reading event data (like the program path), looking up the corresponding rule in the BPF maps, and returning
0
to allow or-EPERM
to block, based on the rule'saction
flag (RULE_DENY
).Events are also reported to the ring buffer (
kubearmor_events
) for logging, similar to the monitoring path.
On the Go side, the BPF-LSM Runtime Enforcer component loads these programs and, crucially, populates the BPF Maps with the translated policies.
Explanation:
loadEnforcerObjects
: Loads the compiled BPF enforcement code.link.AttachLSM
: Attaches a specific BPF program (objs.EnforceProc
) to a named kernel LSM hook (lsm/bprm_check_security
).be.BPFContainerMap = objs.KubearmorContainers
: Gets a handle (reference) to the BPF map defined in the C code. This handle allows the Go program to interact with the map in the kernel.AddContainerPolicies
: This conceptual function shows how KubeArmor translates high-level policies into a kernel-friendly format (e.g., flags likeRULE_DENY
,RULE_EXEC
) and usesBPFContainerMap.Update
to populate the maps. The Namespace IDs (pidns
,mntns
) are used as keys to ensure policies are applied to the correct container context.
This illustrates how KubeArmor uses user-space code to set up the BPF environment in the kernel, loading programs and populating maps. Once this is done, the BPF programs handle enforcement decisions directly within the kernel when events occur.
BPF Components Overview
BPF technology involves several key components:
BPF Programs
Small, safe programs written in a C-like language, compiled to BPF bytecode
Kernel
Monitor events, Enforce policies at hooks
BPF Hooks
Specific points in the kernel where BPF programs can be attached
Kernel
Entry/exit of syscalls, tracepoints, LSM hooks
BPF Maps
Efficient key-value data structures for sharing data
Kernel (accessed by both kernel BPF and user space)
Store policy rules, Store event data (ring buffer), Store identity info
BPF Verifier
Kernel component that checks BPF programs for safety before loading
Kernel
Ensures KubeArmor's BPF programs are safe
BPF JIT
Compiles BPF bytecode to native machine code for performance
Kernel
Makes KubeArmor's BPF operations fast
BPF Loader
User-space library/tool to compile C code, load programs/maps into kernel
User Space
KubeArmor Daemon uses cilium/ebpf
library as loader
Conclusion
In this chapter, you've taken a deeper dive into BPF (eBPF), the powerful kernel technology that forms the backbone of KubeArmor's runtime security capabilities. You learned how eBPF enables KubeArmor to run small, safe, high-performance programs inside the kernel for both observing system events (System Monitor) and actively enforcing security policies at low level hooks (Runtime Enforcer via BPF-LSM). You saw how BPF Maps are used to share data and store policy rules efficiently in the kernel.
Understanding BPF highlights KubeArmor's modern, efficient approach to container and node security. In the next chapter, we'll bring together all the components we've discussed by looking at the central orchestrator on each node
Last updated
Was this helpful?