如何 seccomp 过滤?

解读

在国内 Rust 后端/云原生/安全方向的中高级面试里,“你会怎么做 seccomp 过滤?” 并不是让你背诵 syscall 编号,而是考察三条线:

  1. 是否理解 Linux seccomp-BPF 的编程模型与限制;
  2. 能否用 Rust 生态 把“写规则→编译→注入→运行”做成 可维护、可单元测试、可灰度 的链路;
  3. 是否具备 防御纵深 意识:seccomp 只是其中一环,还要和 capability、namespace、cgroups、容器镜像签名一起构成最小权限。

一句话:让面试官看到“你会用 Rust 把 seccomp 做成产品级安全能力”,而不是“你会调 libc 的 prctl”。

知识点

  1. seccomp 模式

    • mode 1 STRICT:仅能调用 read/write/exit/sigreturn,任何其他 syscall 直接 kill
    • mode 2 FILTER(seccomp-BPF):用户可加载 BPF 程序 做动态判决,返回值有 ALLOW、KILL、TRAP、ERRNO、TRACE、LOG。
  2. Rust 侧核心 crate

    • libc:提供 prctl/PR_SET_NO_NEW_PRIVS、seccomp、dup3 等裸 syscall 绑定。
    • seccomp(libseccomp 的 Rust 绑定):支持 规则优先级、syscall 编号自动解析、arch 自适应,国内镜像源可正常拉取。
    • capscapctl:配套做 capability 裁剪,避免“seccomp 过了但 CAP_SYS_ADMIN 还能逃逸”的尴尬。
  3. eBPF vs seccomp-BPF
    面试官常问“为什么不用 eBPF?”——seccomp-BPF 是经典 cBPF,指令集受限、无 map、无 helper,但上下文切换开销极低;eBPF 用于网络/追踪,不适合高频 syscall 拦截。

  4. 容器场景下的坑

    • --no-new-privs 必须置位,否则 Docker 的 --security-opt seccomp=unconfined 会覆盖掉你辛辛苦苦写的规则。
    • Glibc 封装差异:x86_64 下 open 被 glibc 换成 openat,规则必须按实际 syscall 写,否则“本地测试通,线上容器被 kill”。
  5. Rust 编译期优化
    把规则写成 const 数组,用 lazy_staticonce_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

回答要点

  1. “我选 libseccomp 而不是手写 BPF,因为规则可维护、跨 arch 自动适配。”
  2. “必须在 exec 前设置 PR_SET_NO_NEW_PRIVS,否则 Kubernetes 的 SecurityContext 会覆盖。”
  3. “黑名单只是兜底,白名单更严格,后续可把 Action::Allow 改成 Action::Log 先灰度。”

拓展思考

  1. 规则热升级
    传统 seccomp 一旦 load 不可更改;Rust 侧可再 fork 一个“规则代理”进程,通过 UNIX domain socket 接收新规则,exec 到新版本二进制,实现**“零中断热升级”**。

  2. 与 seccomp-notify 结合
    Linux 5.0+ 支持 seccomp-user-notify,把判决权上提到用户态 Rust 守护线程;对未知 syscall 可先做参数审计再决定放行,实现“可观测的策略运营”——这是国内云厂商**“安全沙箱”**的标配方案。

  3. 与 WebAssembly 联合
    把上述 seccomp 规则编译成 WASM 模块,通过 wasmtime 的 epoch interrupt 机制,在边缘节点实现“可插拔安全策略”;Rust 既是宿主也是客户,同一份代码跑在 x86 边缘和 ARM 网关上,解决嵌入式场景规则一致性问题。