Linux seccomp-bpf 沙箱:从 BPF 过滤器到容器安全实战

Linux seccomp-bpf 沙箱:从 BPF 过滤器到容器安全实战

容器安全并非一蹴而就的单层防护,而是 capability、namespace、seccomp、LSM 与 eBPF 的纵深防御体系。其中,seccomp-bpf 作为系统调用过滤的核心利器,是 Docker、Kubernetes、Podman、systemd 等几乎所有现代 Linux 安全组件的底层基石。本文将从内核机制到实战部署,完整剖析 seccomp-bpf 的设计哲学与工程实现。

---

为什么需要系统调用过滤?

在 Linux 中,用户态进程通过系统调用(syscall)请求内核服务。一个普通进程理论上可以调用 300+ 个 syscall,但它实际可能只需要不到 20 个。如果该进程被攻击者控制,那些从未被使用的 syscall 就变成了潜在的攻击面。

seccomp(Secure Computing Mode)的设计哲学极简:一旦进入 secure 模式,进程只能调用 read()、write()、sigreturn() 和 exit() 四个系统调用。任何违规尝试都会直接触发 SIGKILL 或 SIGSYS,将进程扼杀在内核态。

但纯 seccomp 过于严苛——于是 seccomp-bpf 应运而生。它允许通过 BPF(Berkeley Packet Filter)虚拟机加载自定义过滤规则进程,实现从"全禁止"到"精细化控制"的跨越。

---

BPF 虚拟机:seccomp 的语言心脏

seccomp-bpf 的过滤器是一段在内核 BPF 虚拟机中运行的伪指令集。它接收一个结构体作为输入参数,包含:

struct seccomp_data {
    int nr;                     // 系统调用号
    __u32 arch;                 // 程序架构标识(AUDIT_ARCH_X86_64 等)
    __u64 instruction_pointer;  // 调用时 PC
    __u64 args[6];              // 系统调用参数
};

一条 BPF 过滤器由若干 BPF_STMT(规则)和 BPF_JUMP(跳转)组成。其返回值决定了内核对系统调用的处理方式:

返回值 含义
`SECCOMP_RET_ALLOW` 放行系统调用
`SECCOMP_RET_KILL_PROCESS` (0x80000000) 杀死进程
`SECCOMP_RET_TRAP` 发送 `SIGSYS` 给调用者
`SECCOMP_RET_ERRNO` 返回指定 errno,不执行调用
`SECCOMP_RET_TRACE` 通知 ptrace tracer 处理

---

手写 seccomp-bpf:一个最小沙箱示例

下面是一个不依赖任何库的纯 C 实现,展示如何为一个进程安装 seccomp 过滤器,仅允许 write()、read() 和 exit_group():

#define _GNU_SOURCE
#include <stddef.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>

#define ALLOW(syscall_nr) \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, syscall_nr, 0, 1), \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)

static void install_seccomp_filter(void) {
    struct sock_filter filter[] = {
        /* 加载系统调用号到累加器 */
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),

        /* 校验架构——防止跨架构攻击(x86_64 进程切换到 x86 调用号) */
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),

        /* 白名单 */
        ALLOW(SYS_write),
        ALLOW(SYS_read),
        ALLOW(SYS_exit),
        ALLOW(SYS_exit_group),
        ALLOW(SYS_rt_sigreturn),

        /* 默认:拒绝所有其他调用 */
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
    };

    struct sock_fprog prog = {
        .len = sizeof(filter) / sizeof(filter[0]),
        .filter = filter,
    };

    /* 允许自身被 ptrace 附加(调试需要) */
    if (prctl(PR_SET_NO_NEWPRIVS, 1, 0, 0, 0) < 0) {
        perror("prctl PR_SET_NO_NEWPRIVS");
        _exit(1);
    }

    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) < 0) {
        perror("prctl PR_SET_SECCOMP");
        _exit(1);
    }
}

int main(void) {
    install_seccomp_filter();

    /* 此后只能使用 write/read/exit */
    write(STDOUT_FILENO, "seccomp active\n", 15);
    _exit(0);
}

关键点解析:

1. arch 校验必须优先:不同架构的系统调用号不同。x86_64 上 write 是 0,x86 上却是 4。省略架构检查会遭受 "x86_64 syscall confusion" 攻击。

2. PR_SET_NO_NEWPRIVS:防止通过 execve 提升权限的子进程继承宽松的 seccomp 状态。

3. SECCOMP_RET_KILL_PROCESS:比旧版 SECCOMP_RET_KILL_THREAD 更彻底,直接杀死整个进程组。

---

libseccomp:生产级过滤器构建

手写 BPF 在规则量少时可行,但规则复杂之后维护成本极高。libseccomp 提供了高级抽象,自动生成优化的 BPF 程序:

#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>

void install_container_sandbox(void) {
    scmp_filter_ctx ctx;

    /* 默认动作:返回 EPERM 但不杀死进程(便于应用优雅处理) */
    ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));

    /* 白名单核心调用 */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

    /* 带参数约束的规则:只允许对 fd 0/1/2 调用 read */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 1,
                     SCMP_A0(SCMP_CMP_LE, 2));

    /* 记录敏感系统调用的尝试 */
    seccomp_rule_add(ctx, SCMP_ACT_LOG, SCMP_SYS(execve), 0);
    seccomp_rule_add(ctx, SCMP_ACT_LOG, SCMP_SYS(clone), 0);

    /* 加载到内核 */
    seccomp_load(ctx);
    seccomp_release(ctx);
}

libseccomp 自动处理了 arch 检查、BPF 优化(合并相邻规则、跳转链优化)等底层细节,是 Docker 和 Kubernetes 实际使用的构建方式。

---

Docker 的默认 seccomp Profile

Docker 内置了一个默认 seccomp profile,禁用了约 44 个"高风险"系统调用(如 add_key、keyctl、request_key、bpf、userfaultfd、kexec_load 等)。这个 profile 通过 libseccomp 生成的等效白名单,包含约 300+ 个允许的调用。

可以通过 /proc//status 中的 Seccomp 字段确认进程的 seccomp 状态:

# 检查容器进程的 seccomp 模式
$ cat /proc/12345/status | grep Seccomp
Seccomp:        2       # 2 = SECCOMP_MODE_FILTER

或者在容器运行时使用自定义 profile:

docker run --security-opt seccomp=/path/to/profile.json ubuntu

自定义 JSON profile 的示例片段:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": ["SCMP_ARCH_X86_64"],
    "syscalls": [
        {
            "names": ["read", "write", "close", "exit_group"],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": ["clone", "mount", "ptrace"],
            "action": "SCMP_ACT_LOG"
        }
    ]
}

---

seccomp + Capability + Namespace:纵深防御

单独使用 seccomp 不足以构成完整的安全隔离。现代容器运行时的安全是多层叠加的:

┌──────────────────────────────────────────────┐
│  Layer 1: Namespaces (资源视图隔离)           │
│  隐藏其他进程、网络栈、挂载点、用户ID         │
├──────────────────────────────────────────────┤
│  Layer 2: Capabilities (权限精细化切割)        │
│  摒弃 root 一切特权,按需授予细粒度能力       │
├──────────────────────────────────────────────┤
│  Layer 3: cgroups (资源使用限额)               │
│  PID数、内存、CPU 硬上限                      │
├──────────────────────────────────────────────┤
│  Layer 4: seccomp (系统调用级行为沙箱)        │
│  阻断危险或不必要的内核接口                   │
├──────────────────────────────────────────────┤
│  Layer 5: LSM (强制访问控制)                  │
│  AppArmor/SELinux:文件系统级权限策略         │
└──────────────────────────────────────────────┘

seccomp 的独特价值在于:即使攻击者利用内核漏洞完成了从用户态到内核态的代码执行,那些被 seccomp 标记为 KILL 的系统调用仍然不会执行。研究表明,seccomp 可以阻断约 80% 的内核提权攻击链。

---

Kubernetes 中的 seccomp 部署

Kubernetes 从 1.19 开始将 seccomp 从 alpha 升级到 GA 支持。集群范围的默认行为通过 Pod Security Admission 控制:

Namespace 级别的安全基线配置:

apiVersion: v1
kind: Pod
metadata:
  name: secured-app
  annotations:
    # 强制使用运行时默认 seccomp profile
    seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        capabilities:
          add: ["NET_BIND_SERVICE"]
          drop: ["ALL"]
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault

自定义 seccomp profile 通过 DaemonSet 下发:

apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
  name: my-seccomp
  namespace: default
spec:
  defaultAction: SCMP_ACT_ERRNO
  architectures:
    - SCMP_ARCH_X86_64
  syscalls:
    - names:
        - read
        - write
        - close
        - futex
        - nanosleep
      action: SCMP_ACT_ALLOW

---

与 Landlock、BPF LSM 的关系

seccomp 限制的是系统调用维度,但文件系统访问、网络绑定等操作涉及大量参数检查,seccomp 的 BPF 程序的复杂度和可维护性会成为瓶颈。

新一代 Linux 安全模块提供了不同的抽象层:

`SECCOMP_RET_LOG` 放行但记录到审计日志
安全机制 抽象层级 配置方式
seccomp 系统调用 + 参数 BPF 字节码
Landlock 文件系统访问规则 结构化 API + 规则集

Landlock 的优势在于:它可以在用户态自管理(不需要root配置),且支持规则继承——进程可以在运行中逐步收紧自身的文件系统权限。但 seccomp 作为最早的体系内安全过滤机制,仍然是对抗提权攻击的最后防线。

生产环境的最佳实践是三者叠加:Landlock 控制文件访问、seccomp 过滤系统调用、BPF LSM 补充审计钩子。

---

性能测试与开销

seccomp-bpf 的 BPF 程序在内核执行时经过 JIT 编译为原生指令,每次系统调用的额外开销极小。根据 Linux 内核文档和社区基准测试:

  • 在热路径上(如 `read`/`write`),seccomp 引入的额外延迟约为 **20-80 纳秒**。
  • BPF 检查在进入内核业务逻辑之前完成,过滤规则越多,分支预测失败概率略有上升。
  • 使用 `SECCOMP_RET_LOG`(仅记录不阻断)时,内核会写入审计事件,存在 I/O 开销。
  • 对于高吞吐系统(如 Ceph、Envoy 的数据面),通常默认 seccomp profile 对吞吐影响在 1% 以内,这个成本换来的安全性是完全可接受的。

    ---

    调试技巧

    当 seccomp 导致应用异常时,常用的排查手段:

    # 1. 查看进程被拒绝的系统调用
    $ sudo strace -e trace=seccomp -p <pid>
    
    # 2. 启用内核审计日志,捕获 SECCOMP_RET_TRAP 事件
    $ sudo auditctl -a always,exit -S all -F pid=<pid> -k seccomp_violation
    $ sudo ausearch -k seccomp_violation | aureport -s
    
    # 3. 在 Kubernetes 中,使用 log-only 模式探测哪些调用被阻塞
    # 修改 profile 中的 action 为 SCMP_ACT_LOG,观察日志

    SECCOMP_RET_TRAP 会向进程发送 SIGSYS,常见的 handler 可通过 sigaction 捕获并解析信号栈帧获取违规信息的详细信息(syscall_nr、arch、args),便于在测试阶段精确调整过滤器规则。

    ---

    总结

    seccomp-bpf 以其极简的哲学和强大的执行能力,成为 Linux 安全防御的关键一环。从 Docker 默认 profile 到 Kubernetes Pod Security Standard,从静态 BPF 过滤到与 Landlock、BPF LSM 协同的纵深防御体系,它始终扮演着系统调用级"守门人"的角色。

    理解并正确使用 seccomp-bpf,不只是一个 kernel 机制的问题——它是现代云原生基础设施中,将"信任最小化"原则落地的最直接工程实践。

    代码并非万能,但没有代码的防线等于没有防线。二十一行 BPF 过滤器,或许就是你的最后一条安全边界。
    点赞(0) 打赏

    评论列表 共有 0 条评论

    暂无评论
    BPF LSM 安全钩子点 eBPF + BPF token