Seccomp 的全称是 Secure Computing Mode,是 Linux 内核提供的一种系统调用过滤机制。容器运行时在启动进程前会把 Seccomp Profile 加载进内核,之后进程每次执行系统调用都会先经过规则匹配,策略可以放行、返回错误、直接杀掉进程或只记录日志。默认的 Docker Seccomp Profile 已经禁用了 perf_event_open、clone3 等高风险调用,但默认策略并不一定贴合业务,例如某些数据库或网络工具需要额外的系统调用,如果直接沿用默认配置,可能表现为容器启动后立刻退出或功能异常。

编写自定义 Profile 的难点通常不是 JSON 语法,而是如何在不破坏业务的前提下缩小攻击面。下面从核心字段、实际编写、运行时应用和排错四个角度展开。
Seccomp Profile 的核心结构:defaultAction 与 syscalls
一个完整的 Seccomp Profile 是一个 JSON 文件,最关键的字段是 defaultAction 和 syscalls。defaultAction 决定没有匹配到任何规则时的默认行为,它支持 SCMP_ACT_ALLOW、SCMP_ACT_ERRNO、SCMP_ACT_KILL、SCMP_ACT_LOG 和 SCMP_ACT_NOTIFY 等动作。SCMP_ACT_ALLOW 表示放行,SCMP_ACT_ERRNO 会返回一个 errno 给调用进程,SCMP_ACT_KILL 直接终止进程,SCMP_ACT_LOG 只记录不拦截,而 SCMP_ACT_NOTIFY 可以把决策交给用户态程序。
从安全角度说,默认动作应该尽量收紧。把 defaultAction 设为 SCMP_ACT_ERRNO 或 SCMP_ACT_KILL,再把业务确实需要的系统调用通过 syscalls 数组逐条放行,是比较推荐的做法。如果默认动作是 SCMP_ACT_ALLOW,那整个 profile 基本就失去了过滤意义,只适合在调试阶段临时使用,因为它只是把黑名单里的调用禁掉,其他调用仍然全部放行。
syscalls 数组里每个元素都包含 names、action,还可以包含 args、includes、excludes 等条件字段。names 是系统调用名列表,action 是针对这些调用的动作。下面是一个非常精简的示例,只允许最基本的读写、内存映射和退出调用,其他全部返回 EPERM。
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [
{
"names": [
"read",
"write",
"openat",
"close",
"mmap",
"munmap",
"brk",
"exit",
"exit_group"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
这个模板其实过于严格,很多程序连动态链接库都无法正常加载,但正好能说明默认动作与白名单的关系。真正生产环境使用前,还需要用 strace 抓取业务进程的真实调用,把必要调用补充进去。
动手编写一个最小可用的 Seccomp Profile
从零开始写 profile 的第一步是拿到进程实际使用的系统调用列表。如果应用是以二进制方式运行,可以直接用 strace 跟踪启动过程;如果是解释型语言,比如 Python 或 Node.js,需要跟踪解释器进程,而不是源码脚本。下面命令会把所有调用记录到 /tmp/app.strace,再提取出调用名去重。
strace -f -e trace=all -o /tmp/app.strace ./app grep -oP '\w+(?=\()' /tmp/app.strace | sort -u
拿到调用名清单后,把 defaultAction 设为 SCMP_ACT_ERRNO,再把清单里的调用全部加入 syscalls 数组并设置 action 为 SCMP_ACT_ALLOW。这里有一个容易忽略的地方:很多系统调用在不同架构下编号不同,但 Seccomp Profile 用的是调用名,不是编号,所以写名称通常可以跨架构通用。不过 architectures 字段仍然要列全,否则容器可能会因为架构不匹配而加载失败。
某些系统调用还需要进一步做参数级过滤。比如 ioctl、fcntl、prctl 这类调用功能很多,放行整个调用等于把大门开得很宽。可以在 syscalls 元素中加入 args 数组,指定参数索引、比较运算符和期望值,只允许特定的请求码通过。下面示例只允许 ioctl 的第一个参数等于 TCGETS 的情况,其他请求返回 EPERM。
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["ioctl"],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 1,
"op": "SCMP_CMP_EQ",
"value": 21517,
"valueTwo": 0
}
]
}
]
}
参数过滤的问题在于不同平台上的请求码可能不一样,比如 TCGETS 在 x86_64 上是 0x5401,也就是十进制的 21517,到了 ARM64 上就可能变化。因此,带有 args 条件的 profile 通常需要提前做多架构测试,或者干脆不要在通用 profile 中做过于精细的参数过滤,否则迁移架构后会遇到莫名其妙的权限错误。
在 Docker 和 Kubernetes 中应用 Seccomp Profile
Docker 容器可以直接通过 --security-opt 参数加载自定义 Seccomp Profile。下面命令启动一个 Nginx 容器,并指定本地的 profile 文件路径。路径必须是容器运行时所在主机上的绝对路径,且文件内容要能被 Docker 读取。
docker run --rm --security-opt seccomp=/path/to/my-seccomp.json --name demo nginx
如果希望 Docker 守护进程对某个容器全局生效,可以在 /etc/docker/daemon.json 中配置 seccomp-profile,但这样会影响所有未显式指定 profile 的容器。更常见的做法是在容器编排层面,由 Kubernetes 或 containerd 统一管理 Seccomp 文件。
Kubernetes 使用 securityContext.seccompProfile 字段来指定 Seccomp 策略。其中 type 可以取 RuntimeDefault、Localhost 或 Unconfined。如果需要加载自定义 JSON,就必须使用 Localhost,并把文件放到每个节点上的 /var/lib/kubelet/seccomp 目录中,然后通过 localhostProfile 指定文件名。下面是一个完整的 Pod 示例。
apiVersion: v1
kind: Pod
metadata:
name: seccomp-demo
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: my-seccomp.json
containers:
- name: app
image: nginx
使用 RuntimeDefault 时,容器运行时会加载它自带的默认 Seccomp Profile,不需要你手动维护文件,适合大多数标准工作负载。只有当默认策略跟业务冲突,或者需要更严格限制时,才需要走 Localhost 自定义文件这条路。注意节点上的 seccomp 文件必须真实存在,否则 Pod 会一直卡在 ContainerCreating 状态,kubelet 日志中会提示找不到对应文件。
常见配置错误与排查方法
最常见的问题之一是把 defaultAction 设成 SCMP_ACT_ERRNO 后,容器能启动但应用执行到某个功能就报错。此时不要急着放开全部调用,可以先看进程是否收到了 EPERM 或 EACCES。如果确认是 Seccomp 拦截导致,可以用 strace 观察进程卡在哪个系统调用,也可以直接查看内核审计日志。
grep Seccomp /proc/self/status ausearch -m SECCOMP -ts recent
/proc/self/status 里的 Seccomp 字段值为 0 表示没有启用过滤,值为 1 表示启用严格模式,值为 2 表示启用 filter 模式。容器应用通常都处于 filter 模式。如果值为 0,说明 profile 没有成功加载,需要回头检查启动参数或 Kubernetes 配置。
另一个容易踩坑的点是架构字段缺失。默认的 Docker profile 通常会列出 x86_64、x86 和 x32 三种架构,但在 ARM 节点上运行时,可能需要补充 SCMP_ARCH_ARM64、SCMP_ARCH_ARM 等值。如果 architectures 中没有包含当前进程架构,Seccomp 加载会直接失败,容器启动阶段就会报错。
还要避免用 SCMP_ACT_KILL 覆盖所有未匹配调用。虽然它在安全上更彻底,但会让调试变得非常困难,进程被内核直接杀死时甚至没有 errno 返回值。对大多数业务来说,SCMP_ACT_ERRNO 配合合理的 errno 值,比如 EPERM 或 ENOSYS,更容易暴露问题,也更方便应用层做错误处理。
优化建议:从默认 Docker Profile 出发定制
自己从零拼凑系统调用白名单虽然直观,但非常容易漏掉动态链接器、glibc 初始化、DNS 解析等底层依赖调用。更稳妥的做法是拿容器运行时自带的默认 Seccomp Profile 作为基线,在它基础上做减法或加法。Docker 的默认 profile 文件可以从 moby 项目的源码中找到,containerd 也有类似的默认 seccomp 配置文件,通常位于安装包或源码的 contrib/seccomp 目录下。
拿到默认 profile 后,先把 defaultAction 保持为 SCMP_ACT_ERRNO,然后观察业务是否需要额外的调用。如果某个调用被默认策略拦截,可以在 syscalls 数组中追加一条放行规则;如果某个默认放行的调用明显不需要,可以把它改成 SCMP_ACT_ERRNO 或者从白名单里删掉,同时保留一条显式的 SCMP_ACT_ERRNO 规则做文档说明。
也可以用 seccomp-tools 这类辅助工具直接 dump 正在运行进程的 Seccomp 过滤条件。下面命令会打印出指定 PID 进程当前的 Seccomp 规则,适合在已经跑起来的容器里做反向分析,看看最终生效的策略到底允许了哪些调用。
seccomp-tools dump --pid 1234
自定义 Seccomp Profile 的价值在于把容器权限收敛到业务真正需要的范围。虽然短期会增加一些调试成本,但一旦 profile 稳定下来,就能有效降低内核漏洞被利用的风险。建议把 profile 文件纳入版本控制,和业务代码一起走 CI 流程,每次变更后先在测试环境跑完完整用例,再推广到生产节点。