如何 seccomp 过滤?
解读
在国内 Rust 后端/云原生/安全方向的中高级面试里,“你会怎么做 seccomp 过滤?” 并不是让你背诵 syscall 编号,而是考察三条线:
- 是否理解 Linux seccomp-BPF 的编程模型与限制;
- 能否用 Rust 生态 把“写规则→编译→注入→运行”做成 可维护、可单元测试、可灰度 的链路;
- 是否具备 防御纵深 意识:seccomp 只是其中一环,还要和 capability、namespace、cgroups、容器镜像签名一起构成最小权限。
一句话:让面试官看到“你会用 Rust 把 seccomp 做成产品级安全能力”,而不是“你会调 libc 的 prctl”。
知识点
-
seccomp 模式
- mode 1 STRICT:仅能调用 read/write/exit/sigreturn,任何其他 syscall 直接 kill。
- mode 2 FILTER(seccomp-BPF):用户可加载 BPF 程序 做动态判决,返回值有 ALLOW、KILL、TRAP、ERRNO、TRACE、LOG。
-
Rust 侧核心 crate
- libc:提供 prctl/PR_SET_NO_NEW_PRIVS、seccomp、dup3 等裸 syscall 绑定。
- seccomp(libseccomp 的 Rust 绑定):支持 规则优先级、syscall 编号自动解析、arch 自适应,国内镜像源可正常拉取。
- caps 或 capctl:配套做 capability 裁剪,避免“seccomp 过了但 CAP_SYS_ADMIN 还能逃逸”的尴尬。
-
eBPF vs seccomp-BPF
面试官常问“为什么不用 eBPF?”——seccomp-BPF 是经典 cBPF,指令集受限、无 map、无 helper,但上下文切换开销极低;eBPF 用于网络/追踪,不适合高频 syscall 拦截。 -
容器场景下的坑
- --no-new-privs 必须置位,否则 Docker 的 --security-opt seccomp=unconfined 会覆盖掉你辛辛苦苦写的规则。
- Glibc 封装差异:x86_64 下 open 被 glibc 换成 openat,规则必须按实际 syscall 写,否则“本地测试通,线上容器被 kill”。
-
Rust 编译期优化
把规则写成 const 数组,用 lazy_static 或 once_cell 在启动时一次性加载,避免每次 fork 重新解析;配合 cargo-fuzz 做规则模糊测试,防止 BPF 指令越界。
答案
给出一个可落地的 “Rust + seccomp + 容器” 最小闭环,面试官能直接拷走跑通:
use seccomp::{Action, Context, Rule, Syscall};
use std::process::{Command, Stdio};
use std::os::unix::process::CommandExt;
fn load_filter() -> anyhow::Result<()> {
// 1. 创建上下文:默认拒绝,白名单放行
let mut ctx = Context::new(Action::Allow)?; // 默认 ALLOW,再单独 KILL 危险调用
// 2. 黑名单示例:禁止 ptrace、mount、bpf
let kill_rule = Rule::new(Syscall::from_name("ptrace")?, None, Action::KillProcess);
ctx.add_rule(kill_rule)?;
ctx.add_rule(Rule::new(Syscall::from_name("mount")?, None, Action::KillProcess))?;
ctx.add_rule(Rule::new(Syscall::from_name("bpf")?, None, Action::KillProcess))?;
// 3. 加载进内核
ctx.load()?;
Ok(())
}
fn main() -> anyhow::Result<()> {
// 必须在 clone 之前设置 NO_NEW_PRIVS,否则容器运行时可能覆盖
unsafe {
libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
}
load_filter()?;
// 4. exec 目标业务进程
let err = Command::new("/app/server")
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.exec(); // exec 后 seccomp 继续生效
// exec 只有失败才返回
Err(err.into())
}
Cargo.toml 关键依赖
[dependencies]
seccomp = "0.3" # 对应 libseccomp-sys 3.x,国内 tuna 源可加速
anyhow = "1.0"
libc = "0.2"
Dockerfile 片段
FROM rust:1.78-slim as builder
RUN apt-get update && apt-get install -y libseccomp-dev
COPY . /build
WORKDIR /build
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libseccomp3
COPY --from=builder /build/target/release/seccomp-demo /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/seccomp-demo"]
验证
docker run --rm --security-opt=no-new-privileges:true \
seccomp-demo:latest
# 在容器里执行 ptrace 会直接收到 SIGSYS,进程被杀,dmesg 可见 audit: type=1326
回答要点
- “我选 libseccomp 而不是手写 BPF,因为规则可维护、跨 arch 自动适配。”
- “必须在 exec 前设置 PR_SET_NO_NEW_PRIVS,否则 Kubernetes 的 SecurityContext 会覆盖。”
- “黑名单只是兜底,白名单更严格,后续可把 Action::Allow 改成 Action::Log 先灰度。”
拓展思考
-
规则热升级
传统 seccomp 一旦 load 不可更改;Rust 侧可再 fork 一个“规则代理”进程,通过 UNIX domain socket 接收新规则,exec 到新版本二进制,实现**“零中断热升级”**。 -
与 seccomp-notify 结合
Linux 5.0+ 支持 seccomp-user-notify,把判决权上提到用户态 Rust 守护线程;对未知 syscall 可先做参数审计再决定放行,实现“可观测的策略运营”——这是国内云厂商**“安全沙箱”**的标配方案。 -
与 WebAssembly 联合
把上述 seccomp 规则编译成 WASM 模块,通过 wasmtime 的 epoch interrupt 机制,在边缘节点实现“可插拔安全策略”;Rust 既是宿主也是客户,同一份代码跑在 x86 边缘和 ARM 网关上,解决嵌入式场景规则一致性问题。