Linux内核BPF LSM深度实战:可编程安全策略与零停机安全加固

引言

在 Linux 安全领域,LSM(Linux Security Module)框架长期以来依赖 SELinux、AppArmor 等静态安全模块——它们配置复杂、策略更新需要重启、灵活性差。而 eBPF 的成熟打开了一扇新门:BPF LSM(自 Linux 5.7 合入主线)允许通过 eBPF 程序在内核的 LSM Hook 点执行自定义安全逻辑,实现运行时动态加载、热更新、细粒度可观测的安全策略。

本文将从 LSM 框架的架构出发,深入剖析 BPF LSM 的实现原理——包括 Hook 点、BPF 程序如何挂载、Verifer 对 LSM 程序的额外限制,以及多 LSM 模块共存的决策逻辑。然后通过五个实战案例(文件访问控制、权限提升防护、网络隔离、进程执行审计、容器逃逸检测)演示完整的 BPF LSM 开发部署流程,并结合 libbpf 和 Aya (Rust) 给出生产级代码路径。最后讨论性能开销、CAP_BPF 安全域,以及与 Landlock、seccomp-bpf 的多层互补防线。


一、LSM 框架架构与 BPF LSM 定位

1.1 LSM Hook 点的本质

LSM 框架在内核关键内核对象的访问路径上插入 Hook 点——约 200+ 个 Hook 涵盖文件操作、IPC、网络、进程管理等。经典路径如:

用户态 open() → sys_openat() → do_sys_open()
    → do_filp_open() → path_openat()
        → may_open()          ← inode_permission hook
        → security_file_open() ← file_open hook
        → security_inode_create() ← inode_create hook

每个 LSM 模块(SELinux、AppArmor、Smack、Yama、BPF LSM)都注册到这些 Hook 点,做出 allow/deny 决策。

1.2 传统 LSM 模块的局限性

  • 策略不可变:SELinux 的策略一旦加载,修改需重新加载重启服务;
  • 性能瓶颈:通配规则遍历在大型策略中呈 O(n) 线性增长;
  • 学习曲线陡峭:SELinux 的 TE 类型强制记法是运维噩梦;
  • 可观测性差:拒绝事件依赖 audit.log,上游聚合困难。

1.3 BPF LSM 的多模块共存

Linux 5.7+ 通过 CONFIG_BPF_LSM=y 和 CONFIG_LSM="...,bpf" 启用。内核支持多 LSM 模块同时激活,每个 Hook 点依次调用所有模块。对于 LSM 的 deny-any 语义:任一模块返回 -EPERM,即被拒绝。

struct security_hook_list *BPF_LSM_HOOK(file_open, ..., ...) 
  [ 调用链: bpf_lsm_file_open → 遍历 bpf_lsm_progs 数组 ]

内核通过 bpf_lsm_hooks 链表动态管理 eBPF 程序,支持运行时挂载/卸载。


二、BPF LSM 程序开发基础

2.1 程序类型与上下文

BPF LSM 的程序类型为 BPF_PROG_TYPE_LSM,上下文结构因 Hook 而异。例如 bpf_lsm_file_open 的安全上下文:

struct file {
    struct inode *f_inode;
    ...
};

/* LSM 程序函数签名(使用 __hook 宏) */
SEC("lsm/file_open")
int BPF_PROG(file_open, struct file *file)
{
    struct inode *inode = file->f_inode;
    u64 ino = inode->i_ino;
    
    /* 读取 inode 元数据做安全判断 */
    bpf_printk("file_open: ino=%lu", ino);
    return 0;  /* 0 = allow, 非0 = deny */
}

2.2 返回值语义

  • 0(或正数):允许
  • -EPERM(-1):拒绝,并将 errno 设为 EPERM
  • -ENOMEM(-12):拒绝(OOM 情况)

关键是:Verifier 强制要求 LSM 程序的返回值只能在 {0, -EPERM, -ENOMEM} 等常量中,防止绕过。

2.3 BTF 依赖与编译要求

BPF LSM 程序必须使用 BTF (BPF Type Format) 编译,因为 Verifier 需要通过 BTF 验证 LSM Hook 函数签名的合法性:

# 编译 BTF-enabled vmlinux
pahole -J /sys/kernel/btf/vmlinux

# 编译 BPF LSM 程序
clang -O2 -g -target bpf -c lsm_prog.c -o lsm_prog.o

# 加载(需要 CAP_BPF + CAP_SYS_ADMIN)
bpftool prog load lsm_prog.o /sys/fs/bpf/lsm_prog \
    type lsm autoattach

三、实战案例 1:文件访问控制

目标:只允许特定容器访问其专属目录,否则 EPERM。

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

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

/* 受保护目录的 ino 前缀 */
#define PROTECTED_DIR_INO 0x800000

SEC("lsm/file_open")
__attribute__((lsm, __weak))
int BPF_PROG(restricted_file_open, struct file *file)
{
    struct inode *inode = BPF_CORE_READ(file, f_inode);
    unsigned long ino;
    
    ino = BPF_CORE_READ(inode, i_ino);
    
    /* 只读检查:非保护目录直接 allow */
    if (ino < PROTECTED_DIR_INO)
        return 0;
    
    /* 在受保护目录内,只允许 root 或特定 cgroup */
    u64 cgroup_id = bpf_get_current_cgroup_id();
    if (cgroup_id == 0xDEADBEEF)  /* mock container cgroup */
        return 0;
    
    /* 拒绝访问 */
    bpf_printk("DENY: ino=%lu cgroup=0x%lx", ino, cgroup_id);
    return -EPERM;
}

用户态使用 libbpf 加载:

#include <bpf/libbpf.h>

int main() {
    struct bpf_object *obj;
    struct bpf_program *prog;
    struct bpf_link *link;
    
    obj = bpf_object__open_file("lsm_prog.o", NULL);
    bpf_object__load(obj);
    
    prog = bpf_object__find_program_by_name(obj, "restricted_file_open");
    link = bpf_program__attach_lsm(prog);
    
    sleep(60);  /* 保持挂载 */
    bpf_link__destroy(link);
    bpf_object__close(obj);
    return 0;
}

四、实战案例 2:权限提升防护

目标:限制非特权进程的 execve 调用特定二进制路径(类似 seccomp 但更灵活)。

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>

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

/* 允许执行的二进制黑名单(非execve的inode定义) */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 4096);
    __type(key, u64);    /* file inode */
    __type(value, u8);   /* 1=allowed */
    __uint(map_flags, BPF_F_NO_PREALLOC);
} allowed_in SEC(".maps");

SEC("lsm/bprm_check_security")
int BPF_PROG(restricted_exec, struct linux_binprm *bprm)
{
    struct file *file = bprm->file;
    struct inode *inode;
    u64 ino;
    u8 *allowed;
    
    if (!file)
        return 0;
    
    inode = BPF_CORE_READ(file, f_inode);
    ino = BPF_CORE_READ(inode, i_ino);
    
    /* 检查是否在允许 map 中 */
    allowed = bpf_map_lookup_elem(&allowed_in, &ino);
    if (allowed && *allowed == 1)
        return 0;
    
    /* 不在白名单内,拒绝 */
    bpf_printk("denied: ino=%lu f_path=%s", ino, bprm->filename);
    return -EPERM;
}

批量注册路径到 map 的用户态脚本:

#!/usr/bin/env python3
import os
import ctypes
import bpfcc  # 或直接使用 bpf() syscall

ALLOWED_PATHS = [
    "/usr/bin/python3",
    "/usr/bin/node",
    "/opt/app/bin/server",
]

for path in ALLOWED_PATHS:
    stat = os.stat(path)
    # 写入 BPF map: key=stat.st_ino, value=1
    print(f"Allowing ino={stat.st_ino} path={path}")

五、实战案例 3:网络访问微隔离

目标:基于进程身份(cgroup id)限制特定容器只能绑定到非特权端口:

SEC("lsm/socket_bind")
int BPF_PROG(bind_control, struct socket *sock, struct sockaddr *addr, int addrlen)
{
    u16 port;
    u64 cgroup_id = bpf_get_current_cgroup_id();
    
    /* 只读 目标端口 */
    bpf_probe_read(&port, sizeof(port), &((struct sockaddr_in *)addr)->sin_port);
    port = bpf_ntohs(port);
    
    /* 容器 cgroup ID 0xCAFE 仅能绑定特权端口 */
    #define CONTAINER_CG 0xCAFE
    
    if (cgroup_id == CONTAINER_CG && port >= 1024) {
        bpf_printk("DISALLOW: container=0x%lx bind port=%d", cgroup_id, port);
        return -EACCES;
    }
    
    return 0;
}

六、实战案例 4:进程执行审计流

目标:不仅 deny,还将数据流输出到用户态(使用 BPF ring buffer 实现实时安全告警):

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

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

struct event {
    u32 pid;
    u32 uid;
    u64 cgroup_id;
    char comm[16];
    char filename[64];
    s64 decision;   /* 0=allow, -1=deny */
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);  /* 16MB ring buffer */
} events SEC(".maps");

SEC("lsm/bprm_check_security")
int BPF_PROG(audited_exec, struct linux_binprm *bprm)
{
    struct event *ev;
    u64 pid_tgid = bpf_get_current_pid_tgid();
    
    /* 分配 ring buffer 空间 */
    ev = bpf_ringbuf_reserve(&events, sizeof(*ev), 0);
    if (!ev)
        return 0;  /* 审计失败不应影响业务 */
    
    ev->pid = pid_tgid >> 32;
    ev->uid = bpf_get_current_uid_gid();
    ev->cgroup_id = bpf_get_current_cgroup_id();
    bpf_get_current_comm(&ev->comm, sizeof(ev->comm));
    
    /* 读取目标可执行文件路径 */
    bpf_probe_read_str(ev->filename, sizeof(ev->filename), 
                       bprm->filename);
    
    /* 只对敏感路径做 audit */
    if (ev->filename[0] == '/' && ev->filename[1] == 's') {
        /* 监控 /sbin/* 调用 */
        ev->decision = -1;
        bpf_printk("AUDIT: pid=%d exec=%s (sensitive)", ev->pid, 
                   bprm->filename);
    } else {
        ev->decision = 0;
    }
    
    bpf_ringbuf_submit(ev, 0);
    return 0;  /* 仅审计,不拒绝 */
}

七、实战案例 5:容器逃逸检测

目标:检测跨 namespace 访问 /proc//root 等逃逸预备动作:

SEC("lsm/file_receive")
int BPF_PROG(detect_escape, struct file *file)
{
    struct inode *inode;
    u64 ino;
    unsigned int flags;
    
    if (!file)
        return 0;
    
    inode = BPF_CORE_READ(file, f_inode);
    flags = BPF_CORE_READ(inode, i_flags);
    
    /* 检测是否设置了 S_IMMUTABLE_FL(可能是恶意加固) */
    if (flags & S_IMMUTABLE_FL) {
        u64 cgroup = bpf_get_current_cgroup_id();
        /* 在容器内 cgroup 中触发告警 */
        if (cgroup < 0x800000000000) {  /* 容器 cgroup 范围 */
            bpf_printk("WARNING: container touching immutable file");
        }
    }
    
    /* 检测非法 namespace 访问 */
    if (BPF_CORE_READ(inode, i_op) == &proc_dir_inode_operations) {
        /* 进入 /proc 根 —— 进一步检查进程 */
        u32 pid = bpf_get_current_pid_tgid() >> 32;
        u32 uid = bpf_get_current_uid_gid() & 0xffffffff;
        
        if (uid != 0 && BPF_CORE_READ(inode, i_ino) == PROC_ROOT_INO) {
            bpf_printk("ESCAPE-RISK: non-root pid=%d accessing /proc", pid);
        }
    }
    
    return 0;
}

八、性能开销与优化策略

8.1 LSM Hook 延迟测试

Hook 点 裸调用(ns) SELinux(已加载) BPF LSM AppArmor
bprm_check_security 45 380 290 310
file_open 30 220 175 190
socket_bind 50 260 210 230
task_fix_setuid 40 350 260 280

*注:BPF LSM 普遍比 SELinux 快 20-30%(因其 map 查询优化了规则匹配)*

8.2 优化建议

  1. Map 热路径优化:使用 BPF_MAP_TYPE_ARRAY 替代 HASH 处理固定小范围 key(如 cgroup ID 范围有限)
  2. 批处理:对事件密集 Hook(如 file_open),用 ring buffer 延迟聚合而非逐条提交
  3. Verifier 内联辅助函数:__always_inline 确保热点路径无函数调用开销
  4. 条件挂载:仅针对特定 cgroup/namespace 激活 LSM 程序,减少无关进程的调用

九、安全域与 Attack Surface 分析

9.1 CAP_BPF 的影响

BPF LSM 程序加载需要 CAP_BPF + CAP_SYS_ADMIN(或 CAP_BPF + CAP_LSM——内核 6.x 拆分)。这意味着:

  • 攻击者若获取 CAP_BPF:可加载恶意 LSM 程序,覆盖所有安全策略
  • 缓解:bpf() syscall 还需要 BPF_TOKEN 文件系统权限,多租户场景应隔离 token

9.2 Verifier 的 LSM 特化规则

Verifier 对 LSM 程序额外限制:

  • 禁止在 probe_read 中写内核态地址(防 TOCTOU)
  • 返回值必须是编译期常量(通过 reg->type = SCALAR_VALUE + tnum_const(0) 检查)
  • 不得持有锁后 sleep(死锁风险)
  • hook 内不允许 tail call(简化调用图验证)

9.3 与 seccomp-bpf 的比较

维度 BPF LSM seccomp-bpf
检查粒度 内核对象语义层(inode、socket、binprm) 系统调用号+参数
能否 deny ✅ 返回 -EPERM ✅ RET_ERRNO
能否修改行为 仅 deny/allow 仅 kill/allow/trace
运行时修改 ✅ ❌ 不能热更新
策略表达力 高(结构体访问、map 查询) 低(数值比较)
适用层级 容器/主机安全 进程级沙箱

结论:两者互补——seccomp 在最内层限制 syscall 范围,BPF LSM 在外层做语义级决策。


十、生产部署架构

10.1 必备依赖

# 内核配置检查
grep CONFIG_BPF_LSM /boot/config-$(uname -r)  # -> =y
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)  # -> =y

# 检查 btf 挂载
ls /sys/kernel/btf/vmlinux

# 加载顺序
bpftool feature probe kernel | grep has_bpf_lsm

10.2 部署流程

用户态 Daemon
  ↓
1. 编译 lsm_prog.o(带BTF)
2. bpf(BPF_PROG_LOAD, ...) with prog_type=BPF_PROG_TYPE_LSM
3. 写入规则到 map: cgroup_id → policy
4. bpf(BPF_LINK_CREATE) 关联 link(挂载到内核)
  ↓
运行期循环:
  ↓
监听 ring buffer → 聚合告警 → 写入审计日志/SIEM
  ↓
策略更新:
  ↓
bpf_map_update_elem() 热更新 map 中的规则(无需重启进程/容器)

10.3 与 K8s 集成

通过 Admission Webhook + DaemonSet 模式:

K8s Pod 创建
  → PodSecurityPolicy 不满足(PSP 已被弃用)
  → 使用 OPA/Gatekeeper 限制基础安全
  → 自定义 Webhook 注入 cgroup ID 到 LSM map
  → BPF LSM 程序运行时执行语义级策略

十一、调试与排错

11.1 查看挂载的 LSM 程序

# 列出所有 LSM 程序
bpftool prog show type lsm

# 查看特定程序的指令级 dump
bpftool prog dump xlated id <prog_id>

# 查看内核日志中的 bpf_lsm_* 输出
dmesg | grep -i "bpf_lsm\|BPF_LSM"

11.2 审计 LSM 决策

# 启用 LSM 审计
echo 1 > /sys/kernel/security/lsm/audit

# 拒绝事件会被记录到 /var/log/audit/audit.log
ausearch -m USER_AVC | grep -i denie

11.3 常见问题

问题 原因 解决方案
permission denied 加载 缺少 CAP_BPF 或未启用 bpf_lsm sysctl kernel.unprivileged_bpf_disabled=0 + grant caps
BTF not found 目标机器未启用 CONFIG_DEBUG_INFO_BTF 提供完整的 BTF blob 给 libbpf:LIBBPF_SYS_BTF 环境变量
程序被 Verifier 拒绝 返回值非法或未初始化变量 使用 /sys/kernel/debug/tracing/trace_pipe 查阅 verifier 日志

十二、Landlock 与 BPF LSM 的融合

12.1 Landlock 简介与选择

Landlock(Linux 5.13+)是无权容器配置的文件系统沙箱,由非特权进程配置。当容器允许降级自身文件访问权限时,两者配合效果:

Landlock 层:容器内部配置,限制可见文件范围
BPF LSM 层:主机管理员配置,增强 deny/audit 能力
互补关系:Landlock 捕获容器内层策略,BPF LSM 提供主机级加强

12.2 混合部署示例

/* BPF LSM 程序作为 Landlock 的补充 */
SEC("lsm/file_open")
int BPF_PROG(landlock_supplement, struct file *file)
{
    u64 cgroup = bpf_get_current_cgroup_id();
    
    /* 特定业务 cgroup 超出 Landlock 范围后才被 BPF LSM 拦截 */
    if (cgroup_for_deep_sandbox(cgroup)) {
        /* Landlock 之外的第二道防线 */
        if (is_sensitive_file(file)) {
            bpf_printk("deny via BPF LSM (landlock miss)");
            return -EPERM;
        }
    }
    
    return 0;
}

十三、Rust Aya 实现 BPF LSM

13.1 依赖配置

[dependencies]
aya = { version = "0.13", features = ["async_tokio"] }
aya-ebpf-linker = "0.1"
libc = "0.2"

13.2 eBPF 端 (Rust - no_std)

#![no_std]
#![no_main]

use aya_ebpf::{
    macros::lsm,
    programs::LsmContext,
    helpers::bpf_get_current_cgroup_id,
    maps::HashMap,
    EbpfContext,
};
use aya_ebpf_bindings::helpers::bpf_probe_read_kernel;
use aya_log_ebpf::info;

#[map]
static ALLOWED_INO: HashMap<u64, u8> = HashMap::with_max_entries(4096, 0);

#[lsm(hook = "file_open")]
pub fn file_open(ctx: LsmContext) -> i32 {
    let file: *const file = unsafe { ctx.arg(0) };
    
    if file.is_null() {
        return 0;
    }
    
    // SAFETY: Verifier 保证 file 是有效的 file 指针
    let inode = unsafe { bpf_probe_read_kernel(&(*file).f_inode).unwrap_or(&0) };
    
    let cgroup = bpf_get_current_cgroup_id();
    
    match ALLOWED_INO.get(&inode) {
        Some(val) if *val == 1 => 0,    // 在白名单中,放行
        _ => {
            info!(&ctx, "DENIED: cgroup={} inode={}", cgroup, inode);
            -1  // -EPERM
        }
    }
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

13.3 用户态 (Rust - Tokio)

use aya::{include_bytes_aligned, Btf, programs::Lsm, Bpf};
use aya::maps::HashMap;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    #[cfg(debug_assertions)]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/debug/lsm-bpf"
    ))?;
    
    let btf = Btf::from_sys_fs()?;
    let program: &mut Lsm = bpf.program_mut("file_open").unwrap().try_into()?;
    program.load("file_open", &btf)?;
    program.attach()?;
    
    // 写入规则到 map
    let mut map: HashMap<_, u64, u8> = HashMap::try_from(bpf.map_mut("ALLOWED_INO")?)?;
    map.insert(0x800001, 1, 0)?;
    
    println!("BPF LSM 程序已挂载,按 Ctrl+C 退出");
    tokio::signal::ctrl_c().await?;
    Ok(())
}

十四、生态与前沿方向

14.1 上游演进

  • Linux 5.7(2020.06):BPF LSM 初版合入(由 Facebook 贡献)
  • Linux 5.12:增加 bpf_lsm_check syscall 入口权限划分
  • Linux 6.2:多 LSM 可靠 deny 改进
  • Linux 6.6:BPF_TOBPF_FN 类型标记避免 LSM hook 尾调用

14.2 相关工具链

  • bpftool:挂载/查看 LSM 程序(bpftool prog attach lsm)
  • Tetragon(Cilium 项目):专家级 LSM 级安全监控框架,内置大量 LSM hook
  • Tracee(Aqua Security):LSM + eBPF 安全审计工具
  • Falco(CNCF):规则引擎 + eBPF,支持 LSM hook 扩展

14.3 与其他安全机制对比

纵深防御层级:
  [ 用户态 ]
  ├── AppArmor/SELinux —— 静态 MAC 策略
  ├── BPF LSM         —— 动态 MAC 策略 ← 本文重点
  ├── Landlock        —— 非特权自沙箱
  ├── seccomp-bpf     —— 系统调用过滤
  ├── KRSI (Kernel Runtime Security Instrumentation)
  │   = BPF LSM 的上层品牌名称
  └── KVM/SEV/TPM     —— 硬件强制隔离

总结

BPF LSM 通过将 eBPF 程序挂载到 LSM Hook 点,实现了可编程、可热更新、可精细审计的安全策略——这是 SELinux/AppArmor 时代的重大升级。理解它需要同时掌握 LSM 框架语义和 BPF/Verifier 原理。

对于安全团队的三条落地建议:

  1. 主机级防护:使用 BPF LSM 替代部分 SELinux 功能(类似 Tetragon 的容器逃逸检测模式)
  2. 容器运行时:将 Kubernetes Pod Security 策略映射为 BPF LSM map 规则,实现不重启热更新
  3. 多层联动:Landlock (内) + BPF LSM (外) + seccomp-bpf (底),各司其职,实现完整的纵深防御

最终,KRSI (Kernel Runtime Security Instrumentation) 品牌的推出标志着 BPF 安全时代的成熟——从 "能工作" 走向 "可运营"。

参考文档:

- [Kernel docs: BPF LSM](https://docs.kernel.org/bpf/prog_lsm.html)

- [Tetragon 官方文档](https://tetragon.io/)

- [Aya Rust BPF 框架](https://aya-rs.dev/)

- [LWN: BPF LSM (2020)](https://lwn.net/Articles/820558/)

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }