Seccomp

目录

  1. 概述:什么是 Seccomp
  2. 历史演进:从 cpushare 到容器安全基石
  3. Seccomp 的两种模式
  4. Seccomp-BPF 架构深度解析
  5. Seccomp 用户空间通知(User Notification)
  6. 系统调用接口详解
  7. libseccomp 库:简化 Seccomp 编程
  8. 实战:编写 Seccomp 过滤器程序
  9. Seccomp 在内核中的实现要点
  10. 调试与排错
  11. 安全性分析与局限性
  12. 总结与最佳实践

1. 概述:什么是 Seccomp

Seccomp(SECure COMPuting mode,安全计算模式)是 Linux 内核提供的一种沙箱(Sandbox)机制,用于限制进程可以执行的系统调用。它允许进程自愿进入一个"安全状态",在该状态下只能执行预先允许的系统调用子集。

核心价值

  • 纵深防御:即使应用代码存在漏洞,攻击者能使用的系统调用也受到严格限制,大幅缩小攻击面。
  • 零信任模型:默认拒绝一切,仅显式允许必需的调用(白名单模式)。
  • 极低开销:Seccomp 基于 BPF(Berkeley Packet Filter)在内核中执行,一条 BPF 指令对应一条 CPU 指令,几乎无性能损耗。

与其他安全机制的对比

机制 作用层 控制粒度 典型应用
Seccomp 系统调用入口 精确到每个 syscall 及其参数 容器安全、浏览器沙箱、CTF PWN
AppArmor 文件/路径级 文件和资源的访问控制 Ubuntu 默认 LSM
SELinux 内核对象标签 基于标签的强制访问控制 RHEL/CentOS 默认 LSM
Capabilities 特权操作 拆分 root 特权为细粒度能力 容器权限削减

2. 历史演进:从 cpushare 到容器安全基石

时间 内核版本 里程碑
2005 Linux 2.6.12 Seccomp 首次引入,仅支持 严格模式(Strict Mode)。最初用于 cpushare 项目,允许用户出租空闲 CPU 周期。启用后进程只能使用 read()write()_exit()sigreturn() 四个系统调用。
2007 Linux 2.6.23 内核开始支持 BPF 与 Seccomp 的结合,为过滤模式奠定基础。
2012 Linux 3.5 Seccomp Mode 2(Filter Mode / Seccomp-BPF) 引入。使用 BPF 程序过滤系统调用及其参数,实现灵活的策略定义。Chrome 浏览器率先将其用于渲染进程沙箱。
2017 Linux 4.14 新增 SECCOMP_RET_KILL_PROCESS(杀死整个进程而非仅线程)、SECCOMP_RET_LOG(记录但不拦截)、SECCOMP_FILTER_FLAG_LOG 标志。
2019 Linux 5.0 用户空间通知(User Notification) 机制引入。允许 seccomp 过滤器将特定系统调用转发到用户空间监督进程处理,支持 SECCOMP_IOCTL_NOTIF_RECV/SEND/ADDFD
2020+ Linux 5.x/6.x Seccomp 成为容器安全的核心组件,Docker、Podman、containerd、gVisor、Firecracker 等运行时均深度依赖 Seccomp。

3. Seccomp 的两种模式

3.1 严格模式(Strict Mode)

严格模式是最早的 Seccomp 实现,也是限制最严格的模式。

特性

  • 只允许 4 个系统调用:read()write()_exit()sigreturn()
  • 调用任何其他系统调用都会导致线程被 SIGKILL 立即杀死
  • 不可逆:一旦进入严格模式,无法退出
  • 通过 /proc/PID/seccomp 写入 1 启用(早期接口)

启用方式

#include <linux/seccomp.h>
#include <sys/prctl.h>

// 方式一:使用 prctl()
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

// 方式二:使用 seccomp() 系统调用
#include <linux/seccomp.h>
#include <unistd.h>
#include <sys/syscall.h>

syscall(SYS_seccomp, SECCOMP_SET_MODE_STRICT, 0, NULL);

验证效果

#include <stdio.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>

int main() {
    // 进入严格模式
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

    // 允许:read/write/exit/sigreturn
    write(1, "Hello, Seccomp Strict!\n", 23);

    // 不允许:open() 会导致 SIGKILL
    open("/etc/passwd", O_RDONLY);  // 程序在此处被内核杀死

    return 0;
}

局限性

严格模式过于极端——几乎没有任何实际应用程序能在这种限制下正常工作。因此在实际场景中使用极少,主要用于学术研究和概念验证。


3.2 过滤模式(Filter Mode / Seccomp-BPF)

过滤模式是 Seccomp 的现代形态,也是实际应用中的主流选择。

核心概念

  • 使用 BPF(Berkeley Packet Filter) 程序作为过滤器
  • 过滤器在每个系统调用入口处执行
  • 可以根据系统调用号系统调用参数做精细化判断
  • 支持多种返回值(Action):允许、拒绝、杀死、记录、通知用户空间等

启用前提:no_new_privs

在加载 Seccomp 过滤器之前,进程必须设置 no_new_privs 位。这确保了过滤器不会被应用到比安装者拥有更高权限的子进程上:

prctl(PR_SET_NO_NEW_PRIVS, 1);

原理no_new_privs 阻止进程及其后代通过 execve() 获得新权限(如 SUID 二进制文件)。这防止了低权限进程安装宽松的 seccomp 过滤器后,再通过 SUID 程序逃逸。


4. Seccomp-BPF 架构深度解析

4.1 BPF 程序与 seccomp_data 结构

数据来源

Seccomp-BPF 过滤器在一个 struct seccomp_data 上运行,该结构包含当前系统调用的完整上下文:

struct seccomp_data {
    int   nr;                     /* 系统调用号 */
    __u32 arch;                   /* AUDIT_ARCH_* 值(如 AUDIT_ARCH_X86_64) */
    __u64 instruction_pointer;    /* CPU 指令指针(系统调用指令的地址) */
    __u64 args[6];                /* 最多 6 个系统调用参数 */
};

关键特性struct seccomp_data 不包含任何内存指针,BPF 程序只能评估系统调用号(nr)、架构(arch)和寄存器中的参数值(args[0..5])。这从根本上防止了 TOCTOU(Time-Of-Check-Time-Of-Use)攻击。

BPF 指令结构

每个 BPF 指令定义为:

struct sock_filter {    /* 过滤器指令 */
    __u16 code;         /* 实际的操作码 */
    __u8  jt;           /* 条件为 true 时的跳转偏移 */
    __u8  jf;           /* 条件为 false 时的跳转偏移 */
    __u32 k;            /* 通用多用途字段 */
};

BPF 程序由一组这样的指令组成,封装在 struct sock_fprog 中:

struct sock_fprog {
    unsigned short len;         /* 指令数量 */
    struct sock_filter *filter; /* 指向 BPF 指令数组的指针 */
};

BPF 虚拟机的数据访问

在 seccomp 上下文中,BPF 虚拟机可以访问 seccomp_data 结构体的各个字段。使用 BPF_LDBPF_ABS 组合加载指定偏移量的 32 位数据:

偏移量 字段 说明
0 nr (低 32 位) 系统调用号
4 arch 架构标识
8 instruction_pointer (低 32 位) 指令指针
16 args[0] (低 32 位) 第 1 个参数
24 args[1] (低 32 位) 第 2 个参数
56 args[5] (低 32 位) 第 6 个参数

4.2 返回值(Actions)优先级体系

Seccomp 过滤器返回的 Action 决定了内核对当前系统调用的处理方式。多个过滤器按安装的相反顺序执行,优先级最高的 Action 被采用。

按优先级从高到低:

优先级 Action 内核版本 行为 数据字段
1(最高) SECCOMP_RET_KILL_PROCESS 4.14+ 立即杀死整个进程(所有线程),产生核心转储。状态码为 SIGSYS
2 SECCOMP_RET_KILL_THREAD 3.5+ 立即杀死当前线程。状态码为 SIGSYS
3 SECCOMP_RET_TRAP 3.5+ 向线程发送 SIGSYS 信号(可被捕获)。信号处理程序可通过 siginfo_t 获取被拦截的调用信息 errno 值(通过 siginfo->si_errno
4 SECCOMP_RET_ERRNO 3.5+ 不执行系统调用,直接返回错误。用户空间收到指定的 errno 16 位 errno
5 SECCOMP_RET_USER_NOTIF 5.0+ 将系统调用转发给用户空间监督进程处理。若无监督进程,返回 ENOSYS
6 SECCOMP_RET_TRACE 3.5+ 尝试通知 ptrace 跟踪器。若无跟踪器,返回 ENOSYS 跟踪器可获取
7 SECCOMP_RET_LOG 4.14+ 记录日志后允许执行系统调用
8(最低) SECCOMP_RET_ALLOW 3.5+ 直接允许执行系统调用

优先级规则详解

如果一个系统调用被多个过滤器处理:
  1. 按安装顺序的逆序评估(最新的过滤器先执行)
  2. 取优先级最高的 Action
  3. 如果多个过滤器返回相同优先级的 Action,取最新安装过滤器的 SECCOMP_RET_DATA

/proc 接口

# 查看内核支持的 Action 列表(按优先级降序)
cat /proc/sys/kernel/seccomp/actions_avail
# 输出:kill_process kill_thread trap errno user_notif trace log allow

# 配置哪些 Action 应记录日志
echo "kill_process kill_thread trap errno log" > /proc/sys/kernel/seccomp/actions_logged

# 查看进程的 seccomp 模式
cat /proc/$(pidof nginx)/status | grep Seccomp
# 输出:Seccomp:	2
# 0 = 禁用, 1 = 严格模式, 2 = 过滤模式

4.3 过滤器继承与叠加

继承规则

  • 子进程通过 fork()/clone() 创建时,继承父进程的所有 seccomp 过滤器
  • execve() 不会清除 seccomp 过滤器(与 no_new_privs 配合确保安全)
  • 一旦安装过滤器,无法移除或放宽,只能叠加更多限制

叠加规则

// 过滤器 A:禁止 mount
seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog_a);

// 过滤器 B:额外禁止 ptrace
// 结果:mount 和 ptrace 都被禁止
seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog_b);

关键原则:叠加的过滤器只能进一步收紧限制,永远不能放宽已有限制。这保证了安全策略的单调性。


5. Seccomp 用户空间通知(User Notification)

从 Linux 5.0 开始,Seccomp 支持将特定系统调用转发到用户空间进程处理,这是容器管理器(如 Docker、Podman)实现高级功能的基石。

架构

┌─────────────────┐         ┌──────────────────────┐
│   容器进程       │         │   监督进程(Supervisor)│
│  (Target)       │         │   (容器管理器)         │
│                 │         │                      │
│  发起 mount() ──┼─seccomp─►│  ① 接收通知           │
│                 │  过滤器  │  ② 检查策略           │
│                 │         │  ③ 执行模拟 mount     │
│  得到结果 ◄─────┼─────────┤  ④ 发送响应           │
└─────────────────┘         └──────────────────────┘

核心数据结构

// 通知消息
struct seccomp_notif {
    __u64 id;              /* 唯一标识符 */
    __u32 pid;             /* 触发系统调用的进程 PID */
    __u32 flags;           /* SECCOMP_NOTIF_FLAG_* */
    struct seccomp_data data; /* 系统调用数据 */
};

// 响应消息
struct seccomp_notif_resp {
    __u64 id;              /* 与通知中的 id 匹配 */
    __s64 val;             /* 返回值 */
    __s32 error;           /* errno 值(0 = 成功) */
    __u32 flags;           /* 响应标志 */
};

交互流程

// 1. 安装过滤器时获取通知 fd
struct sock_fprog prog = { ... };
int notify_fd = seccomp(
    SECCOMP_SET_MODE_FILTER,
    SECCOMP_FILTER_FLAG_NEW_LISTENER,
    &prog
);

// 2. 循环接收通知
struct seccomp_notif req;
while (1) {
    memset(&req, 0, sizeof(req));
    ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_RECV, &req);

    // 3. 根据 req.data 做出决策
    // 例如:检查 mount 的目标路径是否在白名单中
    struct seccomp_notif_resp resp = {
        .id = req.id,
        .val = 0,     // 返回值
        .error = 0,   // 0 = 成功, 非 0 = errno
        .flags = 0,
    };

    // 4. 发送响应
    ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_SEND, &resp);
}

高级功能:文件描述符注入

监督进程可以通过 SECCOMP_IOCTL_NOTIF_ADDFD 将文件描述符注入到目标进程:

// 原子性地添加 fd 并发送响应
struct seccomp_notif_addfd addfd = {
    .id = req.id,
    .flags = SECCOMP_ADDFD_FLAG_SEND,    // 原子操作
    .srcfd = fd_to_inject,                // 要注入的 fd
    .newfd = 0,                           // 目标进程中的新 fd 号
};
ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd);

6. 系统调用接口详解

6.1 seccomp() 系统调用

#include <linux/seccomp.h>
#include <unistd.h>
#include <sys/syscall.h>

int syscall(SYS_seccomp,
    unsigned int operation,   /* 操作类型 */
    unsigned int flags,       /* 操作标志 */
    void *args);              /* 参数指针 */

支持的操作

操作 内核版本 功能
SECCOMP_SET_MODE_STRICT 2.6.12 进入严格模式
SECCOMP_SET_MODE_FILTER 3.5 安装 BPF 过滤器
SECCOMP_GET_ACTION_AVAIL 4.14 测试内核是否支持某个 Action
SECCOMP_GET_NOTIF_SIZES 5.0 获取通知结构体的大小

SECCOMP_SET_MODE_FILTER 的标志位

标志 内核版本 功能
SECCOMP_FILTER_FLAG_TSYNC 3.17 同步所有线程的过滤器
SECCOMP_FILTER_FLAG_LOG 4.14 所有非 ALLOW 动作都记录日志
SECCOMP_FILTER_FLAG_NEW_LISTENER 5.0 返回用户空间通知 fd
SECCOMP_FILTER_FLAG_SPEC_ALLOW 4.17 禁用投机性存储绕过缓解
SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV 5.0 通知进程可被信号中断

权限要求

  • 使用 SECCOMP_SET_MODE_FILTER 需要满足以下条件之一:
    1. 调用线程在其用户命名空间中拥有 CAP_SYS_ADMIN 能力,或者
    2. 线程已设置 no_new_privs 位(prctl(PR_SET_NO_NEW_PRIVS, 1)
  • 使用 SECCOMP_SET_MODE_STRICT 同样需要 no_new_privsCAP_SYS_ADMIN

6.2 prctl() 方式

prctl() 是早期的接口,功能是 seccomp() 的子集:

#include <sys/prctl.h>
#include <linux/seccomp.h>

// 进入严格模式
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

// 安装过滤器(无法使用 flags)
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

// 获取当前 seccomp 模式
int mode = prctl(PR_GET_SECCOMP);

建议:优先使用 seccomp() 系统调用,因为它支持所有标志位和新特性。


7. libseccomp 库:简化 Seccomp 编程

直接编写原始 BPF 指令繁琐且容易出错。libseccomp 提供了高级 API,简化过滤器构建。

安装

# Ubuntu/Debian
sudo apt install libseccomp-dev libseccomp2

# CentOS/RHEL
sudo yum install libseccomp-devel

# 编译时链接
gcc -o myapp myapp.c -lseccomp

核心 API

#include <seccomp.h>

// 创建过滤器上下文
scmp_filter_ctx seccomp_init(uint32_t def_action);

// 添加规则
int seccomp_rule_add(
    scmp_filter_ctx ctx,
    uint32_t action,      // 匹配时的动作
    int syscall,          // 系统调用号
    unsigned int arg_cnt, // 参数比较器数量
    ...                   // 比较器列表(成对出现:arg_index, cmp_op, value)
);

// 加载过滤器到内核
int seccomp_load(scmp_filter_ctx ctx);

// 释放上下文
void seccomp_release(scmp_filter_ctx ctx);

// 获取系统调用号
int seccomp_syscall_resolve_name(const char *name);

// 获取架构
uint32_t seccomp_arch_native(void);

// 导出为 BPF 程序(用于保存或传输)
int seccomp_export_bpf(scmp_filter_ctx ctx, int fd);

比较操作符

操作符 含义
SCMP_CMP_NE 不等于
SCMP_CMP_LT 小于
SCMP_CMP_LE 小于等于
SCMP_CMP_EQ 等于
SCMP_CMP_GE 大于等于
SCMP_CMP_GT 大于
SCMP_CMP_MASKED_EQ 掩码后等于

8. 实战:编写 Seccomp 过滤器程序

8.1 使用原始 BPF 编写过滤器

以下示例创建一个过滤器,仅允许 readwriteexit_group,拒绝其他所有系统调用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/syscall.h>
#include <errno.h>

/* BPF 指令宏 */
#define BPF_LABEL(label)       JUMP_JT, JUMP_JF, label, label
#define JUMP_JT                0x00
#define JUMP_JF                0x00

/* 检查架构是否为 x86_64 */
#define ARCH_NR                AUDIT_ARCH_X86_64
#define syscall_nr             (offsetof(struct seccomp_data, args[0]))
#define arch_nr                (offsetof(struct seccomp_data, arch))

#define VALIDATE_ARCHITECTURE \
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, arch_nr), \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ARCH_NR, 1, 0), \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)

#define EXAMINE_SYSCALL \
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, syscall_nr)

#define ALLOW_SYSCALL(name) \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_##name, 0, 1), \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)

#define KILL_PROCESS \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)

int main(int argc, char *argv[]) {
    struct sock_filter filter[] = {
        /* 1. 验证架构 */
        VALIDATE_ARCHITECTURE,

        /* 2. 加载系统调用号 */
        EXAMINE_SYSCALL,

        /* 3. 白名单:允许的系统调用 */
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(exit_group),

        /* 4. 默认拒绝所有其他系统调用 */
        KILL_PROCESS,
    };

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

    /* 先设置 no_new_privs */
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(PR_SET_NO_NEW_PRIVS)");
        exit(EXIT_FAILURE);
    }

    /* 安装 seccomp 过滤器 */
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
        perror("prctl(PR_SET_SECCOMP)");
        exit(EXIT_FAILURE);
    }

    /* 过滤器安装后:以下只有 read/write/exit_group 可用 */
    write(1, "Hello from seccomp sandbox!\n", 28);

    /* 尝试调用 open() —— 会被杀死 */
    // open("/etc/passwd", O_RDONLY);  /* 取消注释将导致 SIGSYS */

    return 0;
}

编译运行:

gcc -o seccomp_raw seccomp_raw.c
./seccomp_raw
# 输出:Hello from seccomp sandbox!
# 如果取消注释 open() 行,程序将被内核杀死

8.2 使用 libseccomp 编写过滤器

同样的逻辑,使用 libseccomp 更简洁清晰:

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

int main(int argc, char *argv[]) {
    int rc;

    /* 初始化过滤器:默认杀死进程 */
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
    if (ctx == NULL) {
        perror("seccomp_init");
        exit(EXIT_FAILURE);
    }

    /* 添加白名单:允许的系统调用 */
    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(exit_group), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);    /* write 内部需要 */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);     /* 内存映射 */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);      /* 堆管理 */

    /* 加载过滤器到内核 */
    rc = seccomp_load(ctx);
    if (rc < 0) {
        fprintf(stderr, "seccomp_load failed: %d\n", rc);
        seccomp_release(ctx);
        exit(EXIT_FAILURE);
    }

    /* 过滤器已生效 */
    write(STDOUT_FILENO, "Sandboxed with libseccomp!\n", 27);

    seccomp_release(ctx);
    return 0;
}

编译运行:

gcc -o seccomp_lib seccomp_lib.c -lseccomp
./seccomp_lib
# 输出:Sandboxed with libseccomp!

带参数过滤的示例

仅允许 write() 写入 stdout(fd=1):

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

int main() {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
    if (!ctx) { perror("init"); exit(1); }

    /* 允许 write() 到 stdout (fd=1) */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
                     SCMP_A0(SCMP_CMP_EQ, 1));  /* arg[0] = fd = 1 */

    /* 允许 exit_group */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

    /* 基础内存和 I/O 调用 */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);

    seccomp_load(ctx);

    /* 只允许写入 stdout */
    write(1, "This goes to stdout\n", 20);
    write(2, "This goes to stderr\n", 20);  /* 被杀死!*/

    seccomp_release(ctx);
    return 0;
}

8.3 Docker 中的 Seccomp 配置

Docker 默认启用一个白名单模式的 seccomp 配置,阻止约 44 个高风险系统调用。

查看默认配置

Docker 的默认 seccomp 配置文件位于:

# Docker 源码中的默认配置
# https://github.com/moby/moby/blob/master/profiles/seccomp/default.json

自定义 Seccomp 配置

黑名单模式(允许大部分调用,阻止特定的):

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    { "names": ["ptrace", "kexec_load", "reboot"], "action": "SCMP_ACT_ERRNO" },
    { "names": ["mount"], "action": "SCMP_ACT_ERRNO" }
  ]
}

白名单模式(仅允许必要调用,生产推荐):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "accept", "bind", "brk", "close", "connect",
        "execve", "exit_group", "fcntl", "fstat",
        "futex", "getdents64", "ioctl", "listen",
        "lseek", "mmap", "mprotect", "munmap",
        "openat", "poll", "read", "rt_sigaction",
        "socket", "stat", "write"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

应用自定义配置:

# 使用自定义 seccomp 配置运行容器
docker run --rm -it \
  --security-opt seccomp=custom-seccomp.json \
  nginx

# 完全禁用 seccomp(仅调试用)
docker run --rm -it \
  --security-opt seccomp=unconfined \
  alpine sh

调试被 Seccomp 拦截的容器

# 查看 seccomp 拦截日志
dmesg | grep -i seccomp

# 使用 strace 发现被拦截的系统调用
strace -c -f docker run --rm nginx 2>&1 | grep -i "operation not permitted"

9. Seccomp 在内核中的实现要点

系统调用入口

在 Linux 内核中,系统调用通过 arch/x86/entry/common.c 或架构特定的入口点进入。Seccomp 检查在系统调用入口路径中非常早的阶段执行:

用户空间 syscall 指令
        │
        ▼
do_syscall_64() / do_fast_syscall_32()
        │
        ▼
__secure_computing()          ← Seccomp 检查点
        │
        ├─ 过滤器返回 ALLOW ──► 执行系统调用
        ├─ 过滤器返回 ERRNO ──► 返回 -errno
        ├─ 过滤器返回 KILL  ──► do_exit(SIGSYS)
        └─ 过滤器返回 TRAP  ──► force_sig(SIGSYS)

BPF 即时编译(JIT)

现代内核将 BPF 程序**即时编译(JIT)**为原生机器码,消除了解释执行的开销:

# 查看 JIT 是否启用
cat /proc/sys/net/core/bpf_jit_enable
# 0 = 禁用, 1 = 启用

# 启用 JIT(需要 root)
echo 1 > /proc/sys/net/core/bpf_jit_enable

线程同步(TSYNC)

SECCOMP_FILTER_FLAG_TSYNC 标志确保进程的所有线程同步到同一个 seccomp 过滤器树,防止多线程环境中的安全漏洞:

seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &prog);
// 如果任何线程无法同步(例如已经处于严格模式),调用返回失败

10. 调试与排错

使用 strace 发现所需系统调用

在构建白名单过滤器之前,先确定应用实际需要哪些系统调用:

# 跟踪所有系统调用
strace -c ./myapp

# 仅跟踪特定调用
strace -e trace=openat,read,write,mmap ./myapp

# 输出到文件分析
strace -o trace.log -f ./myapp

# 提取唯一的系统调用名称
strace -c ./myapp 2>&1 | tail -n +4 | awk '{print $NF}' | sort -u

使用 SECCOMP_RET_LOG 调试

在开发阶段,使用 SECCOMP_RET_LOG 记录被拒绝的调用而不杀死进程:

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["ptrace", "mount", "kexec_load"],
      "action": "SCMP_ACT_LOG"
    }
  ]
}

查看日志:

# 内核日志
dmesg | grep -i seccomp

# systemd 日志
journalctl -k | grep -i seccomp

# 实时监控
dmesg -w | grep seccomp

常见问题诊断

症状 可能原因 解决方案
容器启动后立即退出 Seccomp 阻止了必需的 syscall strace 分析,添加到白名单
Java 应用频繁崩溃 JVM JIT 需要 mmap/mprotect 添加这些调用到白名单
Operation not permitted syscall 被 seccomp 拦截 检查 dmesg 确认被拦截的调用
Invalid argumentseccomp_load() BPF 程序格式错误 检查 struct sock_fprog 结构
无法安装过滤器 未设置 no_new_privs 先调用 prctl(PR_SET_NO_NEW_PRIVS, 1)

11. 安全性分析与局限性

已知绕过技术

虽然 Seccomp 提供了强大的保护,但并非绝对安全:

  1. 通过允许的 syscall 实现恶意行为

    • 例如:openat() + read() + write() 可以读写文件,即使这些调用本身在白名单中
    • 对策:结合 AppArmor/SELinux 限制文件访问路径
  2. 32 位系统调用绕过

    • 64 位进程可以通过 int 0x80 发起 32 位系统调用,其调用号与 64 位不同
    • 对策:在过滤器中检查 arch 字段,拒绝非本机架构
  3. 通过 seccomp 过滤器本身的漏洞

    • 如果过滤器允许 seccomp()prctl(),进程可以修改或移除过滤器
    • 对策:绝不允许 seccomp()prctl() 系统调用
  4. 内核漏洞

    • 如果内核本身存在漏洞,seccomp 无法阻止利用
    • 对策:及时更新内核,保持安全补丁

安全加固建议

Seccomp ─────── 限制系统调用(最后一道防线)
    │
AppArmor ────── 限制文件/路径访问(中间层)
    │
Capabilities ── 削减特权操作(第一层)
    │
no_new_privs ── 防止提权
    │
read-only fs ── 防止文件篡改

12. 总结与最佳实践

关键要点

要点 说明
Seccomp 是内核级系统调用沙箱 在系统调用入口处拦截,无法被用户空间绕过
两种模式 Strict Mode(极简)和 Filter Mode/BPF(灵活)
白名单优于黑名单 默认拒绝 + 显式允许 比 默认允许 + 显式拒绝 更安全
不可逆性 一旦安装过滤器,只能叠加更严格的限制
极低开销 BPF JIT 编译为原生代码,几乎无性能影响
与容器深度集成 Docker/K8s/Podman 均默认使用 Seccomp

生产环境最佳实践

  1. 使用白名单模式"defaultAction": "SCMP_ACT_ERRNO"
  2. 先审计后拦截:用 strace 确定应用所需调用,再构建过滤器
  3. 结合其他安全机制:Seccomp + AppArmor/SELinux + Capabilities + no_new_privs
  4. 使用 libseccomp:避免手写 BPF,减少错误
  5. 为不同服务定制配置:Nginx、MySQL、Redis 需要不同的系统调用集
  6. CI/CD 集成测试:在流水线中验证 seccomp 配置不破坏应用功能
  7. 监控和告警:收集 SECCOMP_RET_LOG 日志,发现异常调用模式

推荐资源


免责声明:本文内容基于 Linux 内核官方文档和公开资料编写,适用于 Linux 2.6.12 ~ 6.x 内核系列。具体 API 行为可能因内核版本和发行版配置而异,请以实际运行环境为准。