---
手写 seccomp-bpf:一个最小沙箱示例
下面是一个不依赖任何库的纯 C 实现,展示如何为一个进程安装 seccomp 过滤器,仅允许 write()、read() 和 exit_group():
#define _GNU_SOURCE
#include <stddef.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#define ALLOW(syscall_nr) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, syscall_nr, 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
static void install_seccomp_filter(void) {
struct sock_filter filter[] = {
/* 加载系统调用号到累加器 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
/* 校验架构——防止跨架构攻击(x86_64 进程切换到 x86 调用号) */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
/* 白名单 */
ALLOW(SYS_write),
ALLOW(SYS_read),
ALLOW(SYS_exit),
ALLOW(SYS_exit_group),
ALLOW(SYS_rt_sigreturn),
/* 默认:拒绝所有其他调用 */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
/* 允许自身被 ptrace 附加(调试需要) */
if (prctl(PR_SET_NO_NEWPRIVS, 1, 0, 0, 0) < 0) {
perror("prctl PR_SET_NO_NEWPRIVS");
_exit(1);
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) < 0) {
perror("prctl PR_SET_SECCOMP");
_exit(1);
}
}
int main(void) {
install_seccomp_filter();
/* 此后只能使用 write/read/exit */
write(STDOUT_FILENO, "seccomp active\n", 15);
_exit(0);
}
关键点解析:
1. arch 校验必须优先:不同架构的系统调用号不同。x86_64 上 write 是 0,x86 上却是 4。省略架构检查会遭受 "x86_64 syscall confusion" 攻击。
2. PR_SET_NO_NEWPRIVS:防止通过 execve 提升权限的子进程继承宽松的 seccomp 状态。
3. SECCOMP_RET_KILL_PROCESS:比旧版 SECCOMP_RET_KILL_THREAD 更彻底,直接杀死整个进程组。
---
libseccomp:生产级过滤器构建
手写 BPF 在规则量少时可行,但规则复杂之后维护成本极高。libseccomp 提供了高级抽象,自动生成优化的 BPF 程序:
#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>
void install_container_sandbox(void) {
scmp_filter_ctx ctx;
/* 默认动作:返回 EPERM 但不杀死进程(便于应用优雅处理) */
ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
/* 白名单核心调用 */
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(close), 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(exit_group), 0);
/* 带参数约束的规则:只允许对 fd 0/1/2 调用 read */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 1,
SCMP_A0(SCMP_CMP_LE, 2));
/* 记录敏感系统调用的尝试 */
seccomp_rule_add(ctx, SCMP_ACT_LOG, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_LOG, SCMP_SYS(clone), 0);
/* 加载到内核 */
seccomp_load(ctx);
seccomp_release(ctx);
}
libseccomp 自动处理了 arch 检查、BPF 优化(合并相邻规则、跳转链优化)等底层细节,是 Docker 和 Kubernetes 实际使用的构建方式。
---
Docker 的默认 seccomp Profile
Docker 内置了一个默认 seccomp profile,禁用了约 44 个"高风险"系统调用(如 add_key、keyctl、request_key、bpf、userfaultfd、kexec_load 等)。这个 profile 通过 libseccomp 生成的等效白名单,包含约 300+ 个允许的调用。
可以通过 /proc//status 中的 Seccomp 字段确认进程的 seccomp 状态:
# 检查容器进程的 seccomp 模式
$ cat /proc/12345/status | grep Seccomp
Seccomp: 2 # 2 = SECCOMP_MODE_FILTER
或者在容器运行时使用自定义 profile:
docker run --security-opt seccomp=/path/to/profile.json ubuntu
自定义 JSON profile 的示例片段:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "close", "exit_group"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["clone", "mount", "ptrace"],
"action": "SCMP_ACT_LOG"
}
]
}
---
seccomp + Capability + Namespace:纵深防御
单独使用 seccomp 不足以构成完整的安全隔离。现代容器运行时的安全是多层叠加的:
┌──────────────────────────────────────────────┐
│ Layer 1: Namespaces (资源视图隔离) │
│ 隐藏其他进程、网络栈、挂载点、用户ID │
├──────────────────────────────────────────────┤
│ Layer 2: Capabilities (权限精细化切割) │
│ 摒弃 root 一切特权,按需授予细粒度能力 │
├──────────────────────────────────────────────┤
│ Layer 3: cgroups (资源使用限额) │
│ PID数、内存、CPU 硬上限 │
├──────────────────────────────────────────────┤
│ Layer 4: seccomp (系统调用级行为沙箱) │
│ 阻断危险或不必要的内核接口 │
├──────────────────────────────────────────────┤
│ Layer 5: LSM (强制访问控制) │
│ AppArmor/SELinux:文件系统级权限策略 │
└──────────────────────────────────────────────┘
seccomp 的独特价值在于:即使攻击者利用内核漏洞完成了从用户态到内核态的代码执行,那些被 seccomp 标记为 KILL 的系统调用仍然不会执行。研究表明,seccomp 可以阻断约 80% 的内核提权攻击链。
---
Kubernetes 中的 seccomp 部署
Kubernetes 从 1.19 开始将 seccomp 从 alpha 升级到 GA 支持。集群范围的默认行为通过 Pod Security Admission 控制:
Namespace 级别的安全基线配置:
apiVersion: v1
kind: Pod
metadata:
name: secured-app
annotations:
# 强制使用运行时默认 seccomp profile
seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
containers:
- name: app
image: myapp:latest
securityContext:
capabilities:
add: ["NET_BIND_SERVICE"]
drop: ["ALL"]
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
自定义 seccomp profile 通过 DaemonSet 下发:
apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
name: my-seccomp
namespace: default
spec:
defaultAction: SCMP_ACT_ERRNO
architectures:
- SCMP_ARCH_X86_64
syscalls:
- names:
- read
- write
- close
- futex
- nanosleep
action: SCMP_ACT_ALLOW
---
与 Landlock、BPF LSM 的关系
seccomp 限制的是系统调用维度,但文件系统访问、网络绑定等操作涉及大量参数检查,seccomp 的 BPF 程序的复杂度和可维护性会成为瓶颈。
新一代 Linux 安全模块提供了不同的抽象层:
| 安全机制 |
抽象层级 |
配置方式 |
| seccomp |
系统调用 + 参数 |
BPF 字节码 |
| Landlock |
文件系统访问规则 |
结构化 API + 规则集 |
| BPF LSM |
安全钩子点 |
eBPF + BPF token |
Landlock 的优势在于:它可以在用户态自管理(不需要root配置),且支持规则继承——进程可以在运行中逐步收紧自身的文件系统权限。但 seccomp 作为最早的体系内安全过滤机制,仍然是对抗提权攻击的最后防线。
生产环境的最佳实践是三者叠加:Landlock 控制文件访问、seccomp 过滤系统调用、BPF LSM 补充审计钩子。
---
性能测试与开销
seccomp-bpf 的 BPF 程序在内核执行时经过 JIT 编译为原生指令,每次系统调用的额外开销极小。根据 Linux 内核文档和社区基准测试:
在热路径上(如 `read`/`write`),seccomp 引入的额外延迟约为 **20-80 纳秒**。
BPF 检查在进入内核业务逻辑之前完成,过滤规则越多,分支预测失败概率略有上升。
使用 `SECCOMP_RET_LOG`(仅记录不阻断)时,内核会写入审计事件,存在 I/O 开销。
对于高吞吐系统(如 Ceph、Envoy 的数据面),通常默认 seccomp profile 对吞吐影响在 1% 以内,这个成本换来的安全性是完全可接受的。
---
调试技巧
当 seccomp 导致应用异常时,常用的排查手段:
# 1. 查看进程被拒绝的系统调用
$ sudo strace -e trace=seccomp -p <pid>
# 2. 启用内核审计日志,捕获 SECCOMP_RET_TRAP 事件
$ sudo auditctl -a always,exit -S all -F pid=<pid> -k seccomp_violation
$ sudo ausearch -k seccomp_violation | aureport -s
# 3. 在 Kubernetes 中,使用 log-only 模式探测哪些调用被阻塞
# 修改 profile 中的 action 为 SCMP_ACT_LOG,观察日志
SECCOMP_RET_TRAP 会向进程发送 SIGSYS,常见的 handler 可通过 sigaction 捕获并解析信号栈帧获取违规信息的详细信息(syscall_nr、arch、args),便于在测试阶段精确调整过滤器规则。
---
总结
seccomp-bpf 以其极简的哲学和强大的执行能力,成为 Linux 安全防御的关键一环。从 Docker 默认 profile 到 Kubernetes Pod Security Standard,从静态 BPF 过滤到与 Landlock、BPF LSM 协同的纵深防御体系,它始终扮演着系统调用级"守门人"的角色。
理解并正确使用 seccomp-bpf,不只是一个 kernel 机制的问题——它是现代云原生基础设施中,将"信任最小化"原则落地的最直接工程实践。
代码并非万能,但没有代码的防线等于没有防线。二十一行 BPF 过滤器,或许就是你的最后一条安全边界。
发表评论 取消回复