如何使用 Landlock?

解读

在国内 Rust 后端、云原生与容器安全方向的面试中,“如何使用 Landlock” 并不是让你背诵系统调用,而是考察三点:

  1. 是否知道 Linux 5.13+ 才内置 Landlock,而 Rust 侧必须通过 landlock crate 做安全封装;
  2. 能否用 Rust 所有权语义 把“规则构建 → 规则合并 → 进程自我沙箱化”三步翻译成类型安全的代码;
  3. 是否理解 “编译期即权限声明” 的 Rust 哲学:规则一旦插入,后续线程即使被攻击也无法扩大权限,这与容器 Seccomp、AppArmor 形成互补。
    面试官常追问:“如果规则写错导致业务日志写不进去,线上如何灰度?” 因此答案必须给出 可回滚、可观测 的落地姿势。

知识点

  • landlock-rs 三件套:Ruleset、PathBeneath、AccessFile 结构体;
  • ABI 版本协商:landlock::ABI::new() 自动降级,保证镜像在 5.10 与 5.15 内核都可启停;
  • Rust 所有权转移:Ruleset::add_rule() 会消费 self,强制“一次性构建”模型,避免运行时追加规则带来的 TOCTOU;
  • 兼容容器场景:与 prctl(NO_NEW_PRIVS) 配合使用,防止通过 setuid 逃逸;
  • 国内镜像源:crates.io 索引走 rsproxy.cn,landlock 0.3+ 已加入“中科大红帽”镜像缓存,CI 无需翻墙。

答案

  1. 依赖与特征开关

    [dependencies]
    landlock = { version = "0.3", features = ["compat"] }
    

    compat 特征 保证旧内核不会编译失败。

  2. 构建最小可用沙箱(只读 /etc,禁止其余写)

    use landlock::{
        AccessFile, PathBeneath, Ruleset, RulesetAttr, RulesetCreated, ABI,
    };
    use std::fs::File;
    use std::os::unix::io::AsRawFd;
    
    fn lock_myself() -> Result<(), anyhow::Error> {
        let abi = ABI::new().unwrap_or(ABI::V1);          // 自动协商
        let ruleset = Ruleset::new()
            .handle_access(AccessFile::from_all(abi))?
            .create()?;                                   // 消费 self,拿到 RulesetCreated
    
        let etc = PathBeneath::new("/etc")
            .allow_access(AccessFile::ReadFile)?;
        let ruleset = ruleset.add_rule(etc)?;             // 继续消费,返回新的 RulesetCreated
    
        ruleset.restrict_self()?;                         // 立即生效,当前线程及其子线程
        Ok(())
    }
    

    关键点:restrict_self() 一旦返回 Ok,当前进程永远无法再打开 /tmp/write.log 进行写操作,除非重启。

  3. 线上灰度与回滚

    • lock_myself() 封装成 “沙箱层”,在 tokio Runtime 启动前调用;
    • 通过 配置中心 下发 landlock.enable 开关,若规则过严导致写日志失败,直接重启 Pod 即可回滚,因为规则只对运行中的进程生效;
    • 利用 prometheus 指标 记录 landlock_restrict_self_result,0 表示成功,1 表示降级,方便 SRE 告警。
  4. 与容器生态协同

    • Kubernetes 1.29+ 中,可把 Landlock 规则写入 OCI seccomp 注解,但 Rust 侧仍建议 自包含,避免不同容器运行时对 Landlock 支持度不一;
    • runc 1.2 的 rootless 容器一起使用时,需先 prctl(NO_NEW_PRIVS),否则 restrict_self() 返回 EPERM

拓展思考

  1. “编译期权限”能否再往前一步?
    借助 const fnserde const 解析,把 JSON 规则在编译期转成 PathBeneath 数组,实现 “镜像构建即沙箱定型”,杜绝运维人员在线改规则带来的合规风险。

  2. 与 io_uring 共存时的死锁陷阱
    io_uring 的 kernel worker 可能在你调用 restrict_self() 之后代表进程打开新文件,若规则未预声明,会返回 EPERM 并触发 uring 回退到 sync 接口,性能陡降 30%。解决:在规则里 预允许 /dev/io_uring 的读写权限,并通过 AccessFile::Execute 禁止任意新二进制执行。

  3. 国内等保 2.0 场景
    等保三级要求“最小权限”,但传统 AppArmor 配置难审计。可把 Landlock 规则导出成 JSON 模板,由 Rust 二进制在启动时打印到 stdout,审计人员直接比对 “代码即策略”,满足 “策略不可变” 条款,且无需宿主机安装额外内核模块。