Seccomp
目录
- 概述:什么是 Seccomp
- 历史演进:从 cpushare 到容器安全基石
- Seccomp 的两种模式
- Seccomp-BPF 架构深度解析
- Seccomp 用户空间通知(User Notification)
- 系统调用接口详解
- libseccomp 库:简化 Seccomp 编程
- 实战:编写 Seccomp 过滤器程序
- Seccomp 在内核中的实现要点
- 调试与排错
- 安全性分析与局限性
- 总结与最佳实践
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_LD 和 BPF_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需要满足以下条件之一:- 调用线程在其用户命名空间中拥有
CAP_SYS_ADMIN能力,或者 - 线程已设置
no_new_privs位(prctl(PR_SET_NO_NEW_PRIVS, 1))
- 调用线程在其用户命名空间中拥有
- 使用
SECCOMP_SET_MODE_STRICT同样需要no_new_privs或CAP_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 编写过滤器
以下示例创建一个过滤器,仅允许 read、write、exit_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 argument 在 seccomp_load() |
BPF 程序格式错误 | 检查 struct sock_fprog 结构 |
| 无法安装过滤器 | 未设置 no_new_privs |
先调用 prctl(PR_SET_NO_NEW_PRIVS, 1) |
11. 安全性分析与局限性
已知绕过技术
虽然 Seccomp 提供了强大的保护,但并非绝对安全:
-
通过允许的 syscall 实现恶意行为
- 例如:
openat()+read()+write()可以读写文件,即使这些调用本身在白名单中 - 对策:结合 AppArmor/SELinux 限制文件访问路径
- 例如:
-
32 位系统调用绕过
- 64 位进程可以通过
int 0x80发起 32 位系统调用,其调用号与 64 位不同 - 对策:在过滤器中检查
arch字段,拒绝非本机架构
- 64 位进程可以通过
-
通过 seccomp 过滤器本身的漏洞
- 如果过滤器允许
seccomp()或prctl(),进程可以修改或移除过滤器 - 对策:绝不允许
seccomp()、prctl()系统调用
- 如果过滤器允许
-
内核漏洞
- 如果内核本身存在漏洞,seccomp 无法阻止利用
- 对策:及时更新内核,保持安全补丁
安全加固建议
Seccomp ─────── 限制系统调用(最后一道防线)
│
AppArmor ────── 限制文件/路径访问(中间层)
│
Capabilities ── 削减特权操作(第一层)
│
no_new_privs ── 防止提权
│
read-only fs ── 防止文件篡改
12. 总结与最佳实践
关键要点
| 要点 | 说明 |
|---|---|
| Seccomp 是内核级系统调用沙箱 | 在系统调用入口处拦截,无法被用户空间绕过 |
| 两种模式 | Strict Mode(极简)和 Filter Mode/BPF(灵活) |
| 白名单优于黑名单 | 默认拒绝 + 显式允许 比 默认允许 + 显式拒绝 更安全 |
| 不可逆性 | 一旦安装过滤器,只能叠加更严格的限制 |
| 极低开销 | BPF JIT 编译为原生代码,几乎无性能影响 |
| 与容器深度集成 | Docker/K8s/Podman 均默认使用 Seccomp |
生产环境最佳实践
- 使用白名单模式:
"defaultAction": "SCMP_ACT_ERRNO" - 先审计后拦截:用
strace确定应用所需调用,再构建过滤器 - 结合其他安全机制:Seccomp + AppArmor/SELinux + Capabilities +
no_new_privs - 使用 libseccomp:避免手写 BPF,减少错误
- 为不同服务定制配置:Nginx、MySQL、Redis 需要不同的系统调用集
- CI/CD 集成测试:在流水线中验证 seccomp 配置不破坏应用功能
- 监控和告警:收集
SECCOMP_RET_LOG日志,发现异常调用模式
推荐资源
免责声明:本文内容基于 Linux 内核官方文档和公开资料编写,适用于 Linux 2.6.12 ~ 6.x 内核系列。具体 API 行为可能因内核版本和发行版配置而异,请以实际运行环境为准。