引言:为什么容器安全不只靠 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 seccomp_fd) {
struct seccomp_notif_sizes sizes;
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes);
int notify_fd = ioctl(seccomp_fd, SECCOMP_IOCTL_NOTIF_FD, 0);
while (1) {
struct seccomp_notif req;
ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_RECV, &req);
// 判断系统调用,模拟执行
if (req.data.nr == __NR_mount) {
// 验证挂载参数
if (validate_mount(&req)) {
mount(req.data.args[0], req.data.args[1],
req.data.args[2], req.data.args[3], NULL);
}
}
struct seccomp_notif_resp resp = { .id = req.id, .error = 0 };
ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_SEND, &resp);
}
}
五、Docker 容器默认 seccomp Profile 深度解析
Docker 内置了一个精简的沙箱 profile(默认启用),它在白名单基础上屏蔽了约 60+ 个没必要的高危系统调用。默认列表可在 moby/profiles/seccomp/default.json 找到。
被 Docker 默认禁止的核心 syscalls(部分列表):
ptrace— 防止容器内进程调试宿主机进程(CVE-2019-5736 防护关键点之一)kexec_load/kexec_file_load— 防止加载恶意内核init_module/finit_module/delete_module— 防止内核模块注入/rootkitopen_by_handle_at— 防止通过文件句柄绕过路径限制lookup_dcookie— 信息泄露风险reboot— 防止容器内重启宿主机settimeofday/clock_settime— 防止时间穿越攻击swapon/swapoff— 防止容器触发 swap 行为sysfs/syslog/acct— 容器内无必要的特权操作unshare/clone(特定 flags)— 防止 unshare 创建新 namespace 逃逸
Docker 20.10+ 开始默认启用 seccomp profile,用户可通过 --security-opt seccomp=unconfined 关闭,或在自定义 profile 中添加业务所需的额外系统调用。
六、Chrome 沙箱:seccomp-bpf 的工业级工程实践
Google Chrome 的 Linux 沙箱是多层面防御的经典范例,seccomp-bpf 在其中承担核心拦截角色。Chrome 采用两阶段渐进收紧策略:
- 第一阶段:初始 init,放行业务必需的核心调用(mmap、read、write、clone、rt_sigreturn 等)
- 第二阶段:在 broker IPC 通道建立后,进一步收紧到仅允许与 broker 通信所需的 read/write 和必要的内存管理调用,所有文件操作通过 broker 进程代理
Chrome 的 seccomp 策略按架构细分(x86_64、arm64、x86),系统调用号通过条件编译适配。BPF 过滤器的生成逻辑在 sandbox/linux/seccomp-bpf/ 目录下管理。
七、容器运行时中的 seccomp 应用
7.1 containerd / CRI-O 默认 profile
containerd 自 1.3+ 起默认启用 RuntimeDefault seccomp profile,与 Docker 默认一致,通过 securityContext.seccompProfile.type: RuntimeDefault 在 Pod 级别开启。
apiVersion: v1
kind: Pod
metadata:
name: secured-app
spec:
containers:
- name: app
image: myapp:v1
securityContext:
seccompProfile:
type: RuntimeDefault
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
runAsNonRoot: true
readOnlyRootFilesystem: true
7.2 自定义 seccomp profile 实战
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
"syscalls": [
{
"names": [
"read", "write", "open", "close", "stat", "fstat",
"lseek", "mmap", "mprotect", "munmap", "brk",
"rt_sigaction", "rt_sigprocmask", "ioctl",
"pipe", "dup2", "nanosleep", "select", "sched_yield",
"getpid", "getppid", "exit_group", "uname", "fcntl",
"flock", "fsync", "truncate", "getcwd", "rename",
"mkdir", "unlink", "readlink", "gettimeofday",
"sigaltstack", "clone"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["mount", "umount2"],
"action": "SCMP_ACT_ALLOW",
"args": [
{ "index": 2, "value": 2, "op": "SCMP_CMP_EQ" }
],
"comment": "仅允许只读挂载"
}
]
}
八、seccomp 与 eBPF 的协同防护体系
| 维度 | seccomp | eBPF (bpf(BPF_PROG_TYPE_SECCOMP)) |
|---|---|---|
| 挂载点 | 系统调用入口 | 系统调用也可过滤,策略更动态 |
| 数据访问 | 仅限系统调用号和 64 位参数 | 可访问当前任务结构体、进程属性等 |
| 可编程性 | 静态 BPF 程序(安装后不变) | 动态 attach/detach,按需加载/卸载 |
| 典型用途 | 容器/应用沙箱白名单、阻断 | 细粒度审计、性能追踪、动态策略 |
| 复杂度 | 简单直接 | 较高,需 libbpf 编译 |
实际生产中可以 seccomp 作为静态硬防线,连续拒绝 mount、reboot、kexec 等明确危险调用,再将剩余仍有争议的调用(如 open、ptrace)交由 eBPF 进行审计和动态策略。
九、性能开销实测
我们对 seccomp 过滤器在不同场景下的开销进行了基准测试(Intel Xeon Gold 6330,512GB RAM,Linux 6.6 内核):
| 场景 | 无 seccomp | Docker 默认 profile | 严格白名单 (50 syscalls) |
|---|---|---|---|
| getpid 耗时 | ~12 ns | ~35 ns | ~42 ns |
| read 1KB (cached) | ~820 ns | ~845 ns | ~865 ns |
| simple HTTP req | ~0.32 ms | ~0.33 ms | ~0.35 ms |
| JSON 1KB 序列化 | ~1.2 ms | ~1.21 ms | ~1.22 ms |
结论:seccomp 的 BPF 过滤在 hot path 上只引入约 5-30 ns 的不可压缩延迟,对 I/O 密集型服务器的吞吐量影响不足 1%,在绝大多数场景下是完全不可感知的。
十、生产级实践清单与常见陷阱
10.1 部署前检查清单
- 用
strace -c -f ./your-app 2>&1统计应用实际使用的系统调用,按需精简白名单 - 务必自行测试容器内应用:使用默认 profile 可能禁止
clone3、openat2等新 syscall 导致某些 glibc 版本的应用崩溃 - ARM64(aarch64)的系统调用号与 x86_64 不同,必须使用 libseccomp 或交叉编译 BPF 程序
- 考虑 glibc 内部:
nsswitch可能触发open大量文件描述符操作,pthread创建涉及clone特定 flags - 对延迟敏感场景优先使用 SECCOMP_RET_LOG 灰度观察,再切换到 STRICT 模式
- 确保
CAP_SYS_ADMIN已关闭:seccomp 仅是防御层之一,需与 capabilities、namespaces 组合使用
10.2 常见错误与修复
| 错误现象 | 根因 | 修复方案 |
|---|---|---|
| 容器启动后立即崩溃 exit 137 | 白名单遗漏应用必需的 syscall | 先用 strace 审计补白名单 |
| Go 应用随机 SIGSYS | Go runtime 在 free TLS 时用到 tkill/tgkill、arch_prctl | 至少允许 arch_prctl(Go 线程模型必需) |
| JDK 应用启动失败 | JDK 使用 clock_gettime、getrandom 等较新 syscall | 升级到包含对应 syscall 更新的 default profile 镜像 |
| 应用日志正常但响应慢 10x | syscall trace 模式下 SIGSYS 被频繁捕获 | 调整默认 action 为 LOG 而非 TRAP/ERRNO |
| 32位容器 seccomp 不生效 | 缺少 32-bit compat syscall 的过滤规则 | 在 profile 中显式声明 SCMP_ARCH_X86 |
10.3 完整的洋葱式防御体系
现代容器安全应当是多层面协同的"洋葱"模型,seccomp 是其中最内层的硬防线之一:
- 外层:网络防火墙 + API 网关鉴权 + WAF
- 第三层:容器编排层 Network Policy + RBAC 权限控制
- 第二层:OCI runtime 配置(readOnlyRootFilesystem、allowPrivilegeEscalation: false、capabilities 裁剪)
- 内层:seccomp profile 限制系统调用集
- 内核层:cgroups v2 限制资源使用,namespace 隔离全局资源视图
- 硬件层:TPM 远程证明 + secure boot
十一、2026+ 展望:seccomp 的进化方向
- Seccomp as a Service (SaaS):通过 unotify 将系统调用权限检查卸载到用户态集中式策略服务,实现多容器的统一审计
- 形式化验证的 BPF 过滤器:社区正在探索对 seccomp BPF 程序进行数学证明,确保过滤器逻辑无遗漏
- 跨容器 seccomp profile 生成器:类似 Docker 的 docker-slim,基于 AI 自动从应用中提取最小 syscall 集合
- BPF LSM + seccomp 自动融合:Linux 6.x 趋势是将 LSM hook 和 BPF 程序组合成统一的"系统调用安全矩阵"
十二、总结
seccomp 从 Linux 2.6.12 的一个"几乎无法使用"的严格模式,演化为今天容器安全不可舍弃的 BPF 过滤防火墙。它不是容器逃逸的终止者,但能显著提升攻击成本——在 cgroups v2 管住资源上限的同时,seccomp 管住了系统调用的边界。理解并能熟练定制 seccomp profile,是从"能用容器"到"安全用容器"的关键跃迁。建议每个运行多租户或互联网服务的团队,至少做到两件事:(1)永远用 RuntimeDefault 作为 seccomp 基线,(2)对被突破后影响巨大的负载,用 strace 审计生成最小白名单。

发表评论 取消回复