Docker核心技术之Seccomp
05 · Seccomp:系统调用过滤
重要程度:★★★☆☆ 进阶 | 适用角色:安全 / 需要自定义 profile 的人 | 前置:04 Capabilities
一、为什么必须学
坑 1:以为"默认 profile 拦了一切危险调用"
不准确。 Docker 默认 seccomp profile 对很多调用是有条件放行的。实测:
# 默认容器 mount 被拒
docker run --rm alpine:3.20 sh -c 'mount -t tmpfs none /tmp'
# mount: permission denied (are you root?)
# 加回 CAP_SYS_ADMIN 后,mount 成功 —— seccomp 并没有拦它
docker run --rm --cap-add=SYS_ADMIN alpine:3.20 sh -c 'mount -t tmpfs none /tmp && echo OK'
# OK
真正拦 mount 的是 capability,不是 seccomp。 不懂这个区分,会在排障时走错方向。
坑 2:自建 profile 时容器直接起不来
docker run --security-opt seccomp=./my.json myapp
# failed to create task for container: ... error during container init:
# error closing exec fds: get handle to /proc/thread-self/fd: fstatfs ... operation not permitted
这个报错信息完全看不出是 seccomp 引起的——因为 runc 初始化阶段调用的系统调用被 profile 拦了。白名单漏一个 fstatfs,容器就起不来。
坑 3:为了调试直接 --security-opt seccomp=unconfined,忘了改回去
等于拆掉了一道防线,且没有任何提示。
二、理论:seccomp 做什么
namespace 管视野,cgroup 管资源,capability 管权限,seccomp 管系统调用。
机制:进程声明"我只允许调用这些系统调用",内核用 BPF 程序在每次系统调用入口处检查。不在白名单里的直接返回 EPERM,连内核代码都不进。
这是容器防御的最后一道防线:即使应用被攻破,mount、reboot、bpf 等调用也调不动。
与 capability 的分工
| 机制 |
管什么 |
粒度 |
| Capabilities |
某个操作需要什么权限 |
41 个能力位 |
| Seccomp |
进程能发起哪些调用 |
系统调用清单(数百个) |
正交关系:capability 说"mount 需要 SYS_ADMIN",seccomp 说"要不要允许 mount 这个调用"。两层独立生效。
三种模式
Seccomp 值 |
模式 |
说明 |
| 0 |
disabled |
未启用 |
| 1 |
strict |
只允许 write / exit / sigreturn |
| 2 |
filter |
BPF 程序过滤,Docker 默认 |
三、实操验证
3.1 三方状态对比
echo "--- 宿主机 ---"
grep -E "Seccomp|NoNewPrivs" /proc/self/status
echo "--- 默认容器 ---"
docker run --rm alpine:3.20 sh -c 'grep -E "Seccomp|NoNewPrivs" /proc/self/status'
echo "--- seccomp=unconfined 容器 ---"
docker run --rm --security-opt seccomp=unconfined alpine:3.20 sh -c \
'grep -E "Seccomp|NoNewPrivs" /proc/self/status'
实测输出:
--- 宿主机 ---
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
--- 默认容器 ---
NoNewPrivs: 0
Seccomp: 2
Seccomp_filters: 1
--- seccomp=unconfined 容器 ---
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
默认容器是 filter 模式,挂 1 个 BPF 过滤器;unconfined 完全禁用。
守护进程配置确认:
docker info | grep -A2 "Security Options"
Security Options:
seccomp
Profile: builtin
Profile: builtin 说明用的是 Docker 内置的默认 profile。
3.2 找到 seccomp 真正拦截的调用
这是本篇最需要实验设计的部分。逐个测试候选调用,对比默认容器和 unconfined 容器:
unshare —— seccomp 独有的拦截
echo "默认容器:"
docker run --rm alpine:3.20 sh -c 'unshare --user echo x 2>&1; echo " 退出码=$?"'
echo "unconfined 容器:"
docker run --rm --security-opt seccomp=unconfined alpine:3.20 sh -c \
'unshare --user echo x 2>&1; echo " 退出码=$?"'
实测输出:
默认容器:
unshare: unshare(0x10000000): Operation not permitted
退出码=1
unconfined 容器:
x
退出码=0
决定性对照:默认容器失败,unconfined 容器成功(输出了 x)。
这直接证明 unshare 是被 seccomp 默认 profile 拦截的——因为唯一变量就是 seccomp 开关。
而 unshare --mount 在两种模式下都失败,说明那个是被 CAP_SYS_ADMIN 拦的(见 04 篇)。两个机制、两个不同的调用,要分开验证。
strace 层的直接观测
docker run --rm alpine:3.20 sh -c '
apk add --no-cache strace >/dev/null 2>&1
strace -f -e trace=unshare,clone,clone3 -o /tmp/t.log unshare --mount echo hi 2>&1 | head -2
grep -E "unshare|clone" /tmp/t.log | head -5
'
unshare: unshare(0x20000): Operation not permitted
--- strace 捕获的调用 ---
18 unshare(CLONE_NEWNS) = -1 EPERM (Operation not permitted)
= -1 EPERM 是 seccomp 拦截的典型特征——内核直接返回错误,系统调用根本没执行。
3.3 自定义 profile:白名单模式
写一个严格的 profile,验证过滤逻辑:
cat > /tmp/sc.json <<'JSON'
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"syscalls": [
{
"names": ["write","exit","exit_group","read","openat","close","futex","mmap",
"mprotect","brk","rt_sigaction","rt_sigprocmask","ioctl","readlink",
"access","clone","set_robust_list","rseq","prlimit64","getrandom",
"madvise","uname","set_tid_address","getpid","gettid","arch_prctl",
"fcntl","dup3","pipe2","poll","ppoll","sched_getaffinity",
"sched_yield","newfstatat","statx","nanosleep","clock_gettime",
"tgkill","rt_sigreturn","getcwd","chdir","openat2","fstatfs",
"fstatfs64","fstat","lstat","stat","mount","umount2","pivot_root",
"chroot","setuid","setgid","setgroups","capset","capget","prctl",
"seccomp","getdents64","lseek","ftruncate","flock","fsync",
"fdatasync","mknod","mknodat","symlink","link","rename","unlink",
"mkdir","rmdir","fchmod","fchown","umask","getuid","geteuid",
"getgid","getegid","setpgid","getsid","getrlimit","setrlimit",
"memfd_create","copy_file_range","sendfile","splice","mremap",
"msync","mlock","munlock","shmget","shmat","shmctl","shmdt",
"semget","semop","semctl","msgget","msgsnd","msgrcv","msgctl",
"sysinfo","getcwd","getpgid","getpgrp","setsid","times","getrusage",
"epoll_create1","epoll_ctl","epoll_pwait","sigaltstack",
"gettid","sched_getparam","sched_getscheduler","prctl","uname"],
"action": "SCMP_ACT_ALLOW"
}
]
}
JSON
profile 结构解读:
| 字段 |
值 |
含义 |
defaultAction |
SCMP_ACT_ERRNO |
不在白名单的调用一律返回错误 |
defaultErrnoRet |
1 |
返回 EPERM |
syscalls[].names |
列表 |
允许的调用 |
syscalls[].action |
SCMP_ACT_ALLOW |
允许 |
这是白名单模式(default-deny)——最安全的策略,与 Docker 内置的默认策略一致。
实测:
docker run --rm --security-opt seccomp=/tmp/sc.json alpine:3.20 sh -c \
'echo "容器正常启动, write 被允许"; exit 0'
容器正常启动, write 被允许
3.4 自定义 profile 的致命坑:漏一个调用容器就起不来
第一次尝试时我的白名单漏了 fstatfs 系列,结果:
docker run --rm --security-opt seccomp=/tmp/sc.json alpine:3.20 sh -c 'echo hi'
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed:
runc create failed: unable to start container process:
error during container init: error closing exec fds:
get handle to /proc/thread-self/fd: fstatfs fsmount:fscontext:proc:
operation not permitted
这个报错完全看不出是 seccomp 引起的,但根因就是白名单里少了 fstatfs / fstatfs64。
runc 在 container init 阶段会调用一系列系统调用(打开 fd、fstat 判断文件类型、挂载伪文件系统等),任何一个不在白名单里,容器就无法启动。
补上 fstatfs、fstatfs64、fstat、lstat、stat 等 stat 系列后,容器正常启动。
排查方法:容器起不来 + 报 operation not permitted → 检查 profile 是否漏了 stat 系列调用。
3.5 常见动作对照
| 动作 |
语法 |
| 禁用 seccomp(仅调试) |
--security-opt seccomp=unconfined |
| 使用自定义 profile |
--security-opt seccomp=/path/to/profile.json |
放宽某个调用(CAP_SYS_ADMIN 等) |
profile 中加 excludes: {caps: ["CAP_SYS_ADMIN"]} |
| 叠加 no-new-privileges |
--security-opt no-new-privileges |
生产环境绝不要用 seccomp=unconfined,等于拆掉一道防线且无任何提示。
四、Docker 默认 profile 概览
Docker 内置 profile 是白名单 + 有限黑名单的混合策略:
- 默认放行:绝大多数常规系统调用(约 300+)
- 默认拒绝:约 44 个已知危险的调用
被拒绝的典型调用:
| 系统调用 |
拒绝原因 |
mount / umount2 |
防止挂载宿主机文件系统(有条件放行,见 3.1) |
reboot |
防止重启宿主机 |
init_module / finit_module |
防止加载内核模块 |
bpf / perf_event_open |
防止提权与侧信道攻击 |
ptrace |
防止注入其他进程 |
kexec_load |
防止加载替代内核 |
open_by_handle_at |
防止绕过路径检查访问文件 |
unshare |
实测被拦(见 3.2),防止创建新 namespace 逃逸 |
clone3 |
曾有 CVE 逃逸,Docker 做了限制 |
keyctl / add_key |
防止内核密钥环操作 |
fanotify_init |
防止监控宿主文件系统 |
为什么 unshare 被拦:容器已经处于 5 个 namespace 中,如果进程能再 unshare 创建新 namespace 并做挂载操作,就可能突破隔离。这是典型的容器逃逸路径。
注意区分:mount 在有 CAP_SYS_ADMIN 时是放行的,unshare 是无条件拒绝。两者性质不同。
定制 profile 的实用建议
从内置 profile 继承,只加不减:
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"syscalls": [
{
"names": ["clone3"],
"action": "SCMP_ACT_ALLOW",
"excludes": { "caps": ["CAP_SYS_ADMIN"] }
}
]
}
这样只针对特定调用做例外,其余仍受保护。
排查起不来的顺序:
容器起不来 + operation not permitted
↓
1. 检查是否用了自定义 seccomp profile
↓
2. 加上 stat 系列:fstatfs, fstatfs64, fstat, lstat, stat, newfstatat, statx
↓
3. 加上进程管理:clone, set_tid_address, set_robust_list, rseq, prlimit64
↓
4. 加上信号:rt_sigaction, rt_sigprocmask, sigaltstack, tgkill, rt_sigreturn
↓
5. 加上内存:mmap, mprotect, brk, madvise, munmap
五、seccomp 与其他安全机制的配合
┌─────────────────────────────────────────────┐
│ 应用代码 │
├─────────────────────────────────────────────┤
│ seccomp ← 系统调用白名单(BPF 过滤) │ 05
├─────────────────────────────────────────────┤
│ capabilities ← 41 个能力位,裁掉 27 个 │ 04
├─────────────────────────────────────────────┤
│ namespace ← 7 个 namespace,6 个独立 │ 01
├─────────────────────────────────────────────┤
│ SELinux/AppArmor ← 路径访问控制标签 │ 系统级
└─────────────────────────────────────────────┘
纵深防御的意义:单层被绕过还有下一层。
以 mount 为例:
- seccomp 层:有
CAP_SYS_ADMIN 时放行
- capability 层:没有
CAP_SYS_ADMIN 就拦下
两层独立生效,任一层配置失误,另一层还能兜住。
六、常见误解
误解 1:seccomp 是"系统调用黑名单"
错,默认是白名单。 Docker 内置 profile 的策略是:
默认放行(约 300+ 常规调用)
+ 显式拒绝(约 44 个危险调用)
这个策略看似是黑名单,但实际上 Docker 内置 profile 也是从 SCMP_ACT_ERRNO 出发逐个列出允许项的,只是允许项太多不便于展示。
自定义 profile 时更是应该坚持 defaultAction: SCMP_ACT_ERRNO 的白名单模式。
误解 2:默认 profile 拦住了 mount
不准确。 实测加回 CAP_SYS_ADMIN 后 mount 成功,seccomp 没拦它。真正拦它的是 capability。
只有 unshare 这类调用是 seccomp 独有的拦截(实测:默认失败,unconfined 成功)。
误解 3:seccomp=unconfined 只是"少点限制",问题不大
问题很大。 它把 BPF 过滤器整个摘掉(Seccomp_filters: 0),容器内的进程可以调用全部系统调用,包括 mount、bpf、perf_event_open 等。
调试用完要立刻改回来,且最好在 CI 里加检查。
误解 4:容器起不来肯定是镜像或配置问题
可能是不完整的 seccomp profile。 报错信息里出现 operation not permitted + error during container init 时,优先怀疑白名单缺了 fstatfs 等基础调用。
误解 5:seccomp 和 capability 谁更严格
没有可比性,是两个维度。 capability 管"权限够不够",seccomp 管"调用放不放行"。
同一次 mount 失败,可能是任一原因——排查时两个都要看:
docker exec <c> grep CapEff /proc/self/status
docker exec <c> grep Seccomp /proc/self/status
七、本篇速查
# 看 seccomp 状态
grep -E "Seccomp|NoNewPrivs" /proc/self/status
docker exec <c> grep -E "Seccomp" /proc/self/status
# 守护进程安全配置
docker info | grep -A2 "Security Options"
# 验证某个调用是否被 seccomp 拦(对照实验)
docker run --rm alpine:3.20 sh -c 'unshare --user echo x; echo "退出码=$?"'
docker run --rm --security-opt seccomp=unconfined alpine:3.20 sh -c 'unshare --user echo x; echo "退出码=$?"'
# strace 观测拦截
docker run --rm alpine:3.20 sh -c '
apk add --no-cache strace >/dev/null 2>&1
strace -f -e trace=unshare -o /tmp/t.log unshare --mount echo hi 2>&1 | head -1
grep unshare /tmp/t.log | head -2
'
# 调试用(用完改回)
docker run --security-opt seccomp=unconfined ...
# 自定义 profile 最小可用模板
cat > /tmp/sc.json <<'JSON'
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"syscalls": [
{ "names": ["write","exit","exit_group","read","openat","close","fstatfs",
"fstatfs64","mmap","mprotect","brk","rt_sigaction","clone",
"set_tid_address","set_robust_list","rseq","prlimit64","ioctl",
"uname","getpid","gettid","arch_prctl","futex","clock_gettime",
"nanosleep","readlink","getcwd","chdir","lseek","ftruncate",
"getdents64","newfstatat","statx","fcntl","dup3","pipe2","poll",
"ppoll","sigaltstack","rt_sigprocmask","tgkill","rt_sigreturn",
"epoll_create1","epoll_ctl","epoll_pwait","openat2","sched_getaffinity",
"sched_yield","madvise","access","getrandom","setuid","setgid",
"setgroups","capset","capget","prctl","seccomp","getuid","geteuid",
"getgid","getegid","getrlimit","setrlimit","gettid","sysinfo"],
"action": "SCMP_ACT_ALLOW" }
]
}
JSON
docker run --security-opt seccomp=/tmp/sc.json ...
---
> 上一篇:[04 · Capabilities:细粒度权限](04-Capabilities-细粒度权限.md)
>
> 下一篇:[06 · 网络实现:veth、网桥与 iptables](06-网络实现-veth网桥与iptables.md)
>
> 相关:[专题索引](README.md)