引言:为什么容器安全不只靠 cgroups?

Docker 容器里的进程真的"隔离"了吗?答案远比你想象的复杂。cgroups v2 解决了资源配额管理(CPU、内存、I/O),但容器内的进程依然共享同一个 Linux 内核,理论上可以发起任何系统调用。如果容器内应用被攻破,攻击者可能通过 ptracekexec_loadinit_module 等系统调用对宿主机发起容器逃逸。seccomp(Secure Computing Mode)正是 Linux 内核提供的系统调用过滤机制,它像一道精密的防火墙,只放行容器"真正需要"的系统调用,从根源上缩小容器的攻击面。

一、seccomp 架构演进:从 Mode 1 到 BPF 过滤器

1.1 初代 seccomp:严格模式(Mode 1)

Linux 2.6.12 引入的第一代 seccomp 只允许 readwrite_exitsigreturn 四个无条件系统调用。这太严苛了,几乎无法用于任何真实应用,因而长期无人问津。

1.2 seccomp-bpf:BPF 赋予的灵活性

Linux 3.5(2012年)引入 seccomp-bpf,将经典的 Berkeley Packet Filter 虚拟机引入系统调用过滤领域。与网络 BPF 过滤数据包类似,seccomp-bpf 过滤系统调用,但可以基于系统调用号及六个 64 位参数进行精细的位运算、比较和跳转。返回值可以是 SECCOMP_RET_ALLOWSECCOMP_RET_KILLSECCOMP_RET_TRAPSECCOMP_RET_ERRNOSECCOMP_RET_TRACESECCOMP_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 发展史上的一次范式转变:被拦截的系统调用不再是简单的"放行/拒绝",而是可以转发到用户态进程由它模拟执行。这为容器运行时实现 mountptracereboot 等受控系统调用提供了可能。

// 简化的 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 — 防止内核模块注入/rootkit
  • open_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 采用两阶段渐进收紧策略:

  1. 第一阶段:初始 init,放行业务必需的核心调用(mmap、read、write、clone、rt_sigreturn 等)
  2. 第二阶段:在 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 的协同防护体系

维度seccompeBPF (bpf(BPF_PROG_TYPE_SECCOMP))
挂载点系统调用入口系统调用也可过滤,策略更动态
数据访问仅限系统调用号和 64 位参数可访问当前任务结构体、进程属性等
可编程性静态 BPF 程序(安装后不变)动态 attach/detach,按需加载/卸载
典型用途容器/应用沙箱白名单、阻断细粒度审计、性能追踪、动态策略
复杂度简单直接较高,需 libbpf 编译

实际生产中可以 seccomp 作为静态硬防线,连续拒绝 mountrebootkexec 等明确危险调用,再将剩余仍有争议的调用(如 openptrace)交由 eBPF 进行审计和动态策略。

九、性能开销实测

我们对 seccomp 过滤器在不同场景下的开销进行了基准测试(Intel Xeon Gold 6330,512GB RAM,Linux 6.6 内核):

场景无 seccompDocker 默认 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 可能禁止 clone3openat2 等新 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 应用随机 SIGSYSGo runtime 在 free TLS 时用到 tkill/tgkill、arch_prctl至少允许 arch_prctl(Go 线程模型必需)
JDK 应用启动失败JDK 使用 clock_gettime、getrandom 等较新 syscall升级到包含对应 syscall 更新的 default profile 镜像
应用日志正常但响应慢 10xsyscall trace 模式下 SIGSYS 被频繁捕获调整默认 action 为 LOG 而非 TRAP/ERRNO
32位容器 seccomp 不生效缺少 32-bit compat syscall 的过滤规则在 profile 中显式声明 SCMP_ARCH_X86

10.3 完整的洋葱式防御体系

现代容器安全应当是多层面协同的"洋葱"模型,seccomp 是其中最内层的硬防线之一:

  1. 外层:网络防火墙 + API 网关鉴权 + WAF
  2. 第三层:容器编排层 Network Policy + RBAC 权限控制
  3. 第二层:OCI runtime 配置(readOnlyRootFilesystem、allowPrivilegeEscalation: false、capabilities 裁剪)
  4. 内层:seccomp profile 限制系统调用集
  5. 内核层:cgroups v2 限制资源使用,namespace 隔离全局资源视图
  6. 硬件层: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 审计生成最小白名单。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部