引言:为什么容器安全不只靠 cgroups?
Docker 容器里的进程真的"隔离"了吗?答案远比你想象的复杂。cgroups v2 解决了资源配额管理(CPU、内存、I/O),但容器内的进程依然共享同一个 Linux 内核,理论上可以发起任何系统调用。如果容器内应用被攻破,攻击者可能通过 ptrace、kexec_load、init_module 等系统调用对宿主机发起容器逃逸。seccomp(Secure Computing Mode)正是 Linux 内核提供的系统调用过滤机制,它像一道精密的防火墙,只放行容器"真正需要"的系统调用,从根源上缩小容器的攻击面。
一、seccomp 架构演进:从 Mode 1 到 BPF 过滤器
1.1 初代 seccomp:严格模式(Mode 1)
Linux 2.6.12 引入的第一代 seccomp 只允许 read、write、_exit 和 sigreturn 四个无条件系统调用。这太严苛了,几乎无法用于任何真实应用,因而长期无人问津。
1.2 seccomp-bpf:BPF 赋予的灵活性
Linux 3.5(2012年)引入 seccomp-bpf,将经典的 Berkeley Packet Filter 虚拟机引入系统调用过滤领域。与网络 BPF 过滤数据包类似,seccomp-bpf 过滤系统调用,但可以基于系统调用号及六个 64 位参数进行精细的位运算、比较和跳转。返回值可以是 SECCOMP_RET_ALLOW、SECCOMP_RET_KILL、SECCOMP_RET_TRAP、SECCOMP_RET_ERRNO、SECCOMP_RET_TRACE 和 SECCOMP_RET_LOG,覆盖了放行、终止、信号、返回错误、通知追踪者和仅记录等所有语义。
关键架构洞察:seccomp-bpf 在内核的
__secure_computing()中,每次系统调用入口都会执行 BPF 程序。BPF 虚拟机保证过滤程序在有限时间内完成(经典 BPF 最大 4096 指令),使系统调用入口的延迟增量控制在百纳秒级。
二、动手编写 seccomp-bpf 过滤程序
2.1 最小可用示例:用 seccomp 禁止 execve
#include
#include
#include
#include
#include
void install_seccomp_filter(void) {
struct sock_filter filter[] = {
// 加载系统调用号到累加器
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
// 如果是 execve (59 on x86_64),杀死进程
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 59, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
// 其他系统调用放行
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
prctl(PR_SET_NO_NEWPRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
2.2 基于参数的高级过滤
// 禁止 clone 带 CLONE_NEWNS | CLONE_NEWUSER flag(防止创建新 namespace)
struct sock_filter namespace_filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 1, 0),
// 不是 clone,放行
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// 是 clone,检查第二个参数(flags)
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, args[0])),
BPF_STMT(BPF_ALU | BPF_AND | BPF_K,
CLONE_NEWNS | CLONE_NEWUSER),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1),
// 包含敏感 flag,拒绝
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & 0xffff)),
// 不包含敏感 flag,放行
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
2.3 使用 libseccomp 安全地构建过滤规则
#include
void build_docker_like_policy(void) {
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认拒绝
// 白名单模式:只允许必需的系统调用
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(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(stat), 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(rt_sigreturn), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
// 对特定调用返回 EPERM 但不杀死(便于进程优雅降级)
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(ptrace), 0);
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mount), 0);
seccomp_load(ctx);
seccomp_release(ctx);
}
三、seccomp 返回值语义与生产级行为
| 返回值 | 行为 | 典型用途 |
|---|---|---|
| SECCOMP_RET_ALLOW | 放行系统调用 | 白名单条目 |
| SECCOMP_RET_KILL_THREAD | 终止调用线程(不会发信号到进程) | 硬安全策略 |
| SECCOMP_RET_KILL_PROCESS | 终止整个进程 | Linux 4.14+ 提供更精确的进程级终止 |
| SECCOMP_RET_TRAP | 发送 SIGSYS 信号 | 可捕获的信号,用于通知父进程 |
| SECCOMP_RET_ERRNO(val) | 系统调用失败并设置 errno | 优雅降级策略 |
| SECCOMP_RET_NOTIF_FD | 通知用户态监听器(seccomp-unotify) | Linux 5.0+,模拟 mount 等调用 |
| SECCOMP_RET_LOG | 记录但放行 | 审计模式,灰度部署 |
四、seccomp-unotify:用户态系统调用模拟
Linux 5.0 引入的 seccomp-unotify 机制(SECCOMP_RET_NOTIF_FD)是 seccomp 发展史上的一次范式转变:被拦截的系统调用不再是简单的"放行/拒绝",而是可以转发到用户态进程由它模拟执行。这为容器运行时实现 mount、ptrace、reboot 等受控系统调用提供了可能。
// 简化的 unotify 工作流
int setup_unotify(int sec

发表评论 取消回复