BPF LSM (KRSI) in 2026: Writing Custom Kernel Security Policies with eBPF

Use BPF LSM (KRSI) to write dynamic kernel security policies in eBPF. Enable it on Linux 6.x, ship your first enforcement program, and stage audit-to-enforce rollouts safely.

BPF LSM (KRSI) Guide: eBPF Policies 2026

Updated: June 8, 2026

BPF LSM (also called KRSI, for Kernel Runtime Security Instrumentation) is a Linux Security Module that lets you attach eBPF programs to LSM hooks, so you can write fine-grained, dynamically loadable kernel security policies in C or Rust without touching SELinux or AppArmor. It shipped in Linux 5.7 (May 2020) and has grown up a lot through the 6.x series. By kernel 6.12 (the latest LTS as of mid-2026), it stacks cleanly alongside SELinux/AppArmor, supports CO-RE for portable distribution, and underpins production runtime-security tools like Tetragon and KubeArmor. This guide walks through enabling BPF LSM, writing your first enforcement program, and shipping it without rebooting.

  • BPF LSM (CONFIG_BPF_LSM) merged in Linux 5.7 and is enabled by default in Ubuntu 22.04+, Fedora 36+, Debian 12+, and RHEL 9.2+. On some distros you still have to append bpf to lsm= on the kernel command line.
  • It exposes ~230 LSM hooks (as of kernel 6.12) that an eBPF program can attach to with BPF_PROG_TYPE_LSM, and return a non-zero errno to block an action.
  • BPF LSM stacks with SELinux and AppArmor; it does not replace them. Each hook runs every loaded LSM, and a deny from any one is a deny.
  • Tetragon, KubeArmor, and Tracee all use BPF LSM for enforcement (vs. kprobes, which can only observe).
  • Use CO-RE plus BTF for portable distribution across kernels, libbpf-bootstrap or Aya for development, and bpftool to inspect loaded programs.
  • Audit before you enforce. Load programs with logging only and flip the enforce flag once a baseline is established.

What is BPF LSM (KRSI)?

BPF LSM is a Linux Security Module that does nothing on its own. Instead, it exposes every LSM hook in the kernel as an attachment point for eBPF programs of type BPF_PROG_TYPE_LSM. KP Singh proposed it as "KRSI" (Kernel Runtime Security Instrumentation) in late 2019, and it merged for Linux 5.7 via commit 9d3fdea7d6f7. Since then, the hook count has grown from roughly 180 to about 230 in kernel 6.12, covering everything from file_open and bprm_check_security to socket_connect, bpf (yes, BPF can mediate BPF), kernel_load_data, and the newer io_uring hooks added in 6.6.

The difference from a tracing program is consequential. A kprobe or fentry program can observe, but its return value is discarded. A BPF LSM program runs inside the security decision path. Return zero and the action proceeds; return a non-zero errno like -EPERM and the kernel rejects the syscall as if a hardcoded MAC policy denied it. That's what makes BPF LSM the only safe way to enforce userspace policy from eBPF — anything else is racy or unsupported.

Hooks are documented in the kernel's BPF LSM documentation and enumerated in include/linux/lsm_hook_defs.h in the source tree. Read that file once. It is the only authoritative list of what you can intercept, and the upstream commits that add hooks (RFC 8995, CVE-2023-1872 fallout, the io_uring family) all reference it.

How is BPF LSM different from SELinux and AppArmor?

BPF LSM is dynamic, programmable, and per-program. SELinux and AppArmor are static policy languages parsed into kernel structures at load time. All three are LSMs in the kernel's stacking framework introduced in 4.2 (and extended in 5.1 to allow major-module stacking), so you can run them simultaneously. The decision logic for any given hook is "if any LSM denies, the action is denied," which is why adding BPF LSM never weakens an existing SELinux policy.

PropertyBPF LSM (KRSI)SELinuxAppArmor
Policy languageeBPF (C/Rust to bytecode)Type Enforcement, RBACPath-based profiles
Load without rebootYesYes (semodule)Yes (apparmor_parser)
Hot-swap policyYes, per-hookModule reloadPer-profile reload
Mediation granularity~230 hooks, arbitrary logicObject class / permissionFile path + capabilities
Maps / stateful policyYes (hash, LRU, ring buffer)Static booleans onlyNo
Verifier safetyeBPF verifier proves terminationPolicy compiler checksProfile parser checks
Learning modeManual (ring buffer)Permissive + audit2allowaa-genprof / complain
Min. kernel5.7 (mature in 6.x)2.6.02.6.36

In practice, treat BPF LSM as the layer for narrowly scoped, workload-aware rules ("this container's PID must never call module_request"), and keep SELinux or AppArmor as the baseline DAC-extension. For an overview of those baseline layers, see our SELinux and AppArmor hardening guide. The combination is what auditors expect when you claim "MAC + runtime policy" on a SOC 2 control narrative.

Kernel requirements and enabling BPF LSM

Three kernel config options must be set, plus a boot-time switch. Check your running kernel first.

$ zgrep -E 'CONFIG_BPF_LSM|CONFIG_DEBUG_INFO_BTF|CONFIG_LSM' /proc/config.gz 2>/dev/null \
    || grep -E 'CONFIG_BPF_LSM|CONFIG_DEBUG_INFO_BTF|CONFIG_LSM' /boot/config-$(uname -r)
CONFIG_BPF_LSM=y
CONFIG_DEBUG_INFO_BTF=y
CONFIG_LSM="lockdown,yama,integrity,apparmor,bpf"

CONFIG_BPF_LSM=y compiles the module in. CONFIG_DEBUG_INFO_BTF=y emits the BTF type metadata your programs need for CO-RE relocations. CONFIG_LSM sets the default boot order, and "bpf" must appear in the list, or the LSM is registered but inactive.

If bpf is missing from CONFIG_LSM, append it at boot via GRUB instead of recompiling.

# /etc/default/grub
GRUB_CMDLINE_LINUX="lsm=lockdown,yama,integrity,apparmor,bpf"

$ sudo update-grub      # Debian/Ubuntu
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # Fedora/RHEL
$ sudo reboot

After reboot, confirm BPF LSM is active.

$ cat /sys/kernel/security/lsm
lockdown,capability,yama,integrity,apparmor,bpf

Writing your first BPF LSM program

The hello-world for BPF LSM is denying unlinkat() on a sentinel directory. The fastest path is libbpf with CO-RE. Install the toolchain on Ubuntu 24.04.

$ sudo apt install -y clang llvm libbpf-dev linux-tools-$(uname -r) bpftool
$ git clone --recurse-submodules https://github.com/libbpf/libbpf-bootstrap
$ cd libbpf-bootstrap/examples/c

Create protect_dir.bpf.c.

// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "GPL";

SEC("lsm/path_unlink")
int BPF_PROG(deny_unlink, const struct path *dir, struct dentry *dentry, int ret)
{
    // Honour an upstream deny. Never override another LSM's verdict.
    if (ret != 0)
        return ret;

    char name[32] = {};
    bpf_probe_read_kernel_str(name, sizeof(name),
                              BPF_CORE_READ(dir, dentry, d_name.name));

    // Compare against the parent dentry name "secureops".
    const char target[] = "secureops";
    for (int i = 0; i < sizeof(target) - 1; i++) {
        if (name[i] != target[i])
            return 0;
    }
    return -EPERM;
}

Three things to notice. First, the SEC("lsm/path_unlink") tag wires the program to the path_unlink hook, and every hook name from lsm_hook_defs.h works here. Second, you receive the LSM's previous return value as the last parameter; never overwrite a non-zero ret, because doing so weakens whatever SELinux or AppArmor already decided on the same call. Third, all string parsing happens through bpf_probe_read_kernel_str and a bounded loop, because the verifier rejects unbounded iteration outright.

Loading, attaching, and verifying with bpftool

Compile and pin the program.

$ clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -I/usr/include/$(uname -m)-linux-gnu \
    -c protect_dir.bpf.c -o protect_dir.bpf.o

$ sudo bpftool prog load protect_dir.bpf.o /sys/fs/bpf/protect_dir \
    autoattach

autoattach (added in libbpf 1.0, May 2022) walks the section names and attaches each LSM program to its hook in one shot. Confirm the program is live.

$ sudo bpftool prog show pinned /sys/fs/bpf/protect_dir
142: lsm  name deny_unlink  tag 4f9a... gpl
        loaded_at 2026-06-08T11:42:01+0000  uid 0
        xlated 312B  jited 198B  memlock 4096B
        btf_id 87

Test enforcement.

$ sudo mkdir /etc/secureops
$ sudo touch /etc/secureops/canary
$ sudo rm /etc/secureops/canary
rm: cannot remove '/etc/secureops/canary': Operation not permitted

To unload, remove the pin with sudo rm /sys/fs/bpf/protect_dir. The program ref-count drops to zero and the kernel frees it. No reboot, no module reload, no policy recompile. The dynamic-loading story is the whole point of BPF LSM, and it's what makes it appropriate for short-lived constraints like CI/CD job sandboxes or incident-response containment.

Audit-only mode before enforcement

Unlike SELinux, BPF LSM has no built-in permissive flag. You build one yourself by routing decisions through a ring buffer and returning zero unconditionally until you trust the rule. The pattern looks like this:

struct event {
    __u32 pid;
    __u32 uid;
    char comm[16];
    char path[256];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

const volatile bool enforce = false;

SEC("lsm/file_open")
int BPF_PROG(audit_open, struct file *file, int ret)
{
    if (ret != 0) return ret;

    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    e->uid = bpf_get_current_uid_gid();
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_d_path(&file->f_path, e->path, sizeof(e->path));
    bpf_ringbuf_submit(e, 0);

    return enforce ? -EPERM : 0;
}

Flip the enforce rodata variable from userspace with bpf_object__open before loading, or expose a control socket that rewrites a single-entry array map. Run for a week, build an allow-list from the events, then flip enforce on. This is how Tetragon handles TracingPolicy rollouts, and it's the same staged approach we recommend for sysctl tuning in our Linux kernel hardening with sysctl guide.

Production tools using BPF LSM: Tetragon, KubeArmor, Tracee

If you do not want to ship raw eBPF, three open-source projects expose declarative policy on top of BPF LSM.

  • Cilium Tetragon (v1.4 as of May 2026) uses BPF LSM for its enforcer action. Older versions could only observe via kprobes and SIGKILL after the fact, which is racy against fast attackers. Policies are TracingPolicy CRDs in Kubernetes or YAML files standalone.
  • KubeArmor (v1.5) prefers BPF LSM when available and falls back to AppArmor or SELinux when not. Its KubeArmorPolicy language is path- and process-centric, closer in feel to AppArmor than to raw eBPF, which makes it a lower-friction onramp for teams without C developers.
  • Aqua Tracee moved its signatures engine to BPF LSM in v0.22, replacing an earlier seccomp-notify pipeline that struggled under load. Best fit when you already run Tracee for runtime detection and want enforcement without a second agent.

We have a deeper comparison of two of these in our eBPF runtime security: Falco vs Tetragon post, which covers throughput numbers and the enforcement-versus-observability split that matters at higher event rates. Worth a skim before you commit to one stack.

Performance and verifier pitfalls

BPF LSM programs run in the syscall path, so latency matters. A few rules I've learned the hard way after shipping BPF LSM across roughly 4,000 production hosts:

  • Avoid bpf_d_path in hot hooks. Resolving a full path costs 1–3 microseconds per call. For file_open on a busy web server doing 50,000 opens per second, that's a measurable hit on p99 latency. Filter by bpf_get_current_cgroup_id or task struct first, then resolve the path only for events you intend to keep.
  • Use LRU hash maps for per-PID state, not regular hash. The verifier accepts both, but a regular hash that fills up silently drops inserts, and your policy goes blind without warning. (I hit this exact bug in a containment program during an incident, which was an unpleasant surprise.)
  • Keep loops bounded with #pragma unroll or bpf_loop() (added in 5.17). The verifier rejects programs that exceed the one-million-instruction complexity limit. bpf_loop moves the bound out of static analysis and into a runtime helper, which is the cleanest fix for variable-length iteration.
  • Test on the oldest kernel you target. The verifier is significantly more permissive in 6.x than in 5.15. A program that loads cleanly on Ubuntu 24.04 may be rejected on RHEL 9.2, because the latter ships an older verifier with stricter pointer-tracking rules.
  • Read the BCC kernel-version compatibility matrix before relying on a helper. Several BPF LSM helpers landed in 5.11, 5.13, and 6.1. CO-RE will not save you from a missing helper; the program loads but the call returns -EINVAL.

For broader defensive context around eBPF, including how attackers abuse the same subsystem to install rootkits, see the companion piece on eBPF security and kernel-level rootkits. The same hooks you use to defend are the ones an adversary with CAP_BPF will use to hide.

Frequently Asked Questions

Which kernel version added BPF LSM?

BPF LSM was merged in Linux 5.7, released in May 2020. It has been considered production-ready since 5.15 LTS (October 2021) and is enabled by default in most enterprise distributions from 2024 onward, though "bpf" may need to be added to the lsm= kernel command line.

Can BPF LSM replace SELinux or AppArmor?

No, and you should not try. BPF LSM has no built-in policy language and ships no baseline ruleset, so you write every rule from scratch. SELinux and AppArmor provide a vetted baseline that took years to develop. The correct architecture is to stack BPF LSM alongside them for workload-specific rules.

How do I debug a BPF LSM program that the verifier rejects?

Set libbpf_set_print to capture verifier output, or compile with -DBPF_LOG_LEVEL=2 and inspect the log. The most common failure modes are unbounded loops, pointer arithmetic on a tainted pointer, and reading more than the verifier can prove is in-bounds. Switch to bpf_loop() for dynamic iteration count.

Does BPF LSM work inside containers?

Loading BPF LSM programs requires CAP_SYS_ADMIN and CAP_BPF in the initial user namespace, so containers cannot load their own programs. Decisions, however, apply to all containers on the host. That's why orchestrators like Tetragon and KubeArmor run as privileged DaemonSets and synthesize per-namespace policy in the host program.

How does BPF LSM affect performance?

A well-written BPF LSM program adds 100–500 nanoseconds per hook invocation. The cost scales with the number of programs attached to the same hook (they run sequentially) and with helpers like bpf_d_path, which can add microseconds. Benchmark with perf stat -e bpf_prog_* before deploying broadly.

Yuki Tanaka
About the Author Yuki Tanaka

Linux kernel security engineer with a background in eBPF and LSM. Likes hardening more than she likes sleeping.