Linux 内核 Landlock LSM:无特权文件系统沙箱的工程化安全实战

当容器的 breakout 只需要一个错误配置的 /proc/self/exe 符号链接,当 root 权限已经不再是安全边界——我们需要一种机制,让非特权用户自己在沙箱里"画地为牢"。Landlock 正是 Linux 内核给出的答案。


一、为什么我们仍然需要 Landlock

2023 年发现 runc 容器逃逸漏洞 CVE-2024-21626(Leaky Vessels),攻击者通过覆盖 /proc/self/exe 实现容器逃逸。2024 年继续爆出多个容器运行时漏洞——Docker、containerd、CRI-O 谁都未能幸免。

传统纵深防御栈应对容器逃逸的能力十分有限:

  • User Namespace:虽然将容器内 root 映射到宿主机非特权用户,但内核中 user namespace 与某些 LSM 模块的交互边界仍然存在问题,且无法实现细粒度文件访问控制。
  • SELinux / AppArmor:需要 root 加载策略规则,策略编写复杂且难以在容器内动态调整。
  • seccomp-bpf:仅过滤系统调用,不感知文件路径语义——一个被允许 open() 的进程仍然可以打开任意文件。
  • Capabilities:粗粒度的权限切割,CAP_DAC_READ_SEARCH 一旦授予就等于绕过了所有 DAC 检查。

Landlock 从设计哲学上不同:让进程自己定义自己的访问约束,且这些约束只能收紧不能放宽,并且会继承给所有子进程。

这种"权限只能递减"的单调性保证,使得安全边界在 exec() 穿越时依然有效——这是之前任何机制都无法同时满足的特性。


二、Landlock 核心架构设计

Landock 从 Linux 5.13 进入主线内核,到 6.8 版本已经支持 7 种访问权限标志,覆盖了文件系统操作的大部分攻击面。

2.1 三层抽象模型

Landlock 的安全模型由三层构成:

Ruleset(规则集)
  ├── Access Rights(访问权限定义)
  └── Rules(规则条目)
        ├── 目标:目录或文件路径
        └── 允许的动作:_FS_ACCESS_PERMS_*

Ruleset 是权限策略的容器,一个进程(及其所有后代)绑定一个 active ruleset。

Access Rights 在 ruleset 创建时声明——你只能对 ruleset 声明的权限类型追加规则,后续无法扩大权限范围。

Rules 将具体路径与允许操作绑定,只有显式 allow 的路径才能被访问。

2.2 权限类型全景

截至内核 6.8:

权限标志 语义
LANDLOCK_ACCESS_FS_EXECUTE 文件执行
LANDLOCK_ACCESS_FS_WRITE_FILE 文件写入
LANDLOCK_ACCESS_FS_READ_FILE 文件读取
LANDLOCK_ACCESS_FS_READ_DIR 目录遍历与读取
LANDLOCK_ACCESS_FS_REMOVE_DIR 删除目录
LANDLOCK_ACCESS_FS_REMOVE_FILE 删除文件
LANDLOCK_ACCESS_FS_MAKE_CHAR 创建字符设备
LANDLOCK_ACCESS_FS_MAKE_DIR 创建目录
LANDLOCK_ACCESS_FS_MAKE_REG 创建普通文件
LANDLOCK_ACCESS_FS_MAKE_SOCK 创建 Unix socket
LANDLOCK_ACCESS_FS_MAKE_FIFO 创建 FIFO
LANDLOCK_ACCESS_FS_MAKE_BLOCK 创建块设备
LANDLOCK_ACCESS_FS_MAKE_SYM 创建符号链接
LANDLOCK_ACCESS_FS_REFER 文件跨目录 rename() / link()
LANDLOCK_ACCESS_FS_TRUNCATE 文件 truncate()

2.3 不可变继承机制(monotonicity)

这是 Landlock 最关键的安全特性:

// 一旦通过 prctl(PR_SET_NO_NEW_PRIVS, 1) 关闭了新特权获取,
// 且通过 landlock_restrict_self() 应用了规则集后,
// 该进程及其所有子进程都无法移除或放宽已设置的约束。

// 子进程可以追加更多限制(add more rules),但绝不能减少。
// 这个属性通过 ruleset 的 "handled_access" 位掩码单调递增来保证。

三、编程接口深度解析

3.1 最低限度工作流

#include <linux/landlock.h>
#include <sys/prctl.h>
#include <sys/syscall.h>

#define ACCESS_FS_READ_WRITE \
    (LANDLOCK_ACCESS_FS_EXECUTE | \
     LANDLOCK_ACCESS_FS_READ_FILE | \
     LANDLOCK_ACCESS_FS_READ_DIR | \
     LANDLOCK_ACCESS_FS_WRITE_FILE | \
     LANDLOCK_ACCESS_FS_REMOVE_FILE | \
     LANDLOCK_ACCESS_FS_REMOVE_DIR | \
     LANDLOCK_ACCESS_FS_MAKE_DIR | \
     LANDLOCK_ACCESS_FS_MAKE_REG | \
     LANDLOCK_ACCESS_FS_REFER | \
     LANDLOCK_ACCESS_FS_TRUNCATE)

int main(void) {
    // 步骤 1:禁止 future privilege escalation
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        return 1;
    }

    // 步骤 2:创建 ruleset,声明能管理的所有权限类型
    struct landlock_ruleset_attr ruleset_attr = {
        .handled_access = ACCESS_FS_READ_WRITE,
    };
    int ruleset_fd = syscall(SYS_landlock_create_ruleset,
                             &ruleset_attr, sizeof(ruleset_attr), 0);
    if (ruleset_fd < 0) {
        perror("landlock_create_ruleset");
        return 1;
    }

    // 步骤 3:添加允许访问的目录规则
    struct landlock_path_beneath_attr path_attr = {
        .allowed_access = ACCESS_FS_READ_WRITE,
        .parent_fd = open("/app/data", O_PATH | O_CLOEXEC),
    };
    if (syscall(SYS_landlock_add_rule, ruleset_fd,
                LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0)) {
        perror("landlock_add_rule");
        return 1;
    }
    close(path_attr.parent_fd);

    // 步骤 4:应用规则集到当前进程
    if (syscall(SYS_landlock_restrict_self, ruleset_fd, 0)) {
        perror("landlock_restrict_self");
        return 1;
    }
    close(ruleset_fd);

    // 至此——即使当前进程是 root,也只能操作 /app/data 下的文件。
    write_to_etc();  // → EACCES
    read_ssh_keys(); // → EACCES
    write_app_data(); // → OK
    return 0;
}

3.2 关键 API 语义要点

路径必须通过 O_PATH 打开。规则添加时传递 parent_fd 而非路径字符串,这意味着 Landlock 在内核中持有 inode 引用——即使后续文件系统上该路径被替换、挂载覆盖或符号链接重定向,Landlock 绑定的 inode 依然有效。这从根本上消除了 TOCTOU 竞争条件。

目录规则的 Landlock 语义:对 /app/data 添加 rule,表示"允许在以 /app/data 为根的子树内执行指定的操作"。但注意:

// 正确理解:规则作用于路径 inode 的下级树
// 1. 对 /app/data 本身的操作,其语义取决于具体类型
// 2. 目录的 WRITE_FILE 仅作用于直接子项的 fd,不递归
// 3. REMOVE_DIR / REMOVE_FILE 需要父目录的 write 权限(通过 dir rule 实现)

3.3 权限粒度选择策略

生产环境不应一次性开启全部权限——最小权限原则应当从需求出发:

// Web 静态文件服务器——只需要读取与执行
#define MINIMAL_WEB \
    (LANDLOCK_ACCESS_FS_READ_FILE | \
     LANDLOCK_ACCESS_FS_READ_DIR | \
     LANDLOCK_ACCESS_FS_EXECUTE)

// CI/CD 构建缓存——需要读写但不需删除
#define BUILD_CACHE \
    (LANDLOCK_ACCESS_FS_READ_FILE | \
     LANDLOCK_ACCESS_FS_READ_DIR | \
     LANDLOCK_ACCESS_FS_WRITE_FILE | \
     LANDLOCK_ACCESS_FS_MAKE_DIR | \
     LANDLOCK_ACCESS_FS_MAKE_REG)

// 日志收集 agent——只需在指定目录创建追加写文件
#define LOG_AGENT \
    (LANDLOCK_ACCESS_FS_WRITE_FILE | \
     LANDLOCK_ACCESS_FS_MAKE_REG | \
     LANDLOCK_ACCESS_FS_TRUNCATE)

四、实战:构建最小权限容器沙箱

我们将编写一个 C 库 libsandbox.so,通过 LD_PRELOAD 注入到任意进程中,实现透明的 filesystem 沙箱化。

4.1 设计目标

  • 非侵入式:目标进程无需重新编译
  • 可配置:通过环境变量 SANDBOX_ROOT、SANDBOX_ACCESS 控制
  • 可组合:与现有 secprofile 并存,不冲突

4.2 核心实现

// libsandbox.c —— 通过 __attribute__((constructor)) 在目标进程启动前执行
#define _GNU_SOURCE
#include <dlfcn.h>
#include <linux/landlock.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

static int create_landlock_ruleset(uint6_t access_mask) {
    struct landlock_ruleset_attr attr = { .handled_access = access_mask };
    return (int)syscall(SYS_landlock_create_ruleset, &attr, sizeof(attr), 0);
}

static int add_directory_rule(int ruleset_fd, const char *path, uint64_t access) {
    struct landlock_path_beneath_attr rule = { .allowed_access = access };
    rule.parent_fd = open(path, O_PATH | O_CLOEXEC);
    if (rule.parent_fd < 0) {
        fprintf(stderr, "[sandbox] Failed to open %s: %s\n", path, strerror(errno));
        return -1;
    }
    int rc = (int)syscall(SYS_landlock_add_rule, ruleset_fd,
                          LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
    close(rule.parent_fd);
    return rc;
}

static uint64_t parse_access_env(const char *env_str) {
    if (!env_str) return LANDLOCK_ACCESS_FS_READ_FILE |
                        LANDLOCK_ACCESS_FS_READ_DIR  |
                        LANDLOCK_ACCESS_FS_WRITE_FILE |
                        LANDLOCK_ACCESS_FS_EXECUTE;
    uint64_t mask = 0;
    // 简化解析:逗号分隔的权限名,此处省略实现细节
    if (strstr(env_str, "READ_FILE"))  mask |= LANDLOCK_ACCESS_FS_READ_FILE;
    if (strstr(env_str, "READ_DIR"))   mask |= LANDLOCK_ACCESS_FS_READ_DIR;
    if (strstr(env_str, "WRITE_FILE")) mask |= LANDLOCK_ACCESS_FS_WRITE_FILE;
    if (strstr(env_str, "EXECUTE"))    mask |= LANDLOCK_ACCESS_FS_EXECUTE;
    if (strstr(env_str, "REMOVE"))     mask |= LANDLOCK_ACCESS_FS_REMOVE_FILE |
                                             LANDLOCK_ACCESS_FS_REMOVE_DIR;
    if (strstr(env_str, "MAKE"))       mask |= LANDLOCK_ACCESS_FS_MAKE_REG |
                                             LANDLOCK_ACCESS_FS_MAKE_DIR;
    return mask;
}

__attribute__((constructor(101))) // 在大多数 constructor 之前执行
static void sandbox_init(void) {
    // 检查 Landlock 是否可用(内核 >= 5.13 且未禁用)
    if (access("/proc/sys/kernel/landlock", F_OK) != 0 &&
        prctl(PR_GET_SECCOMP) == 0) {
        // 可能在老旧内核上,静默跳过
        return;
    }

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);

    uint64_t access = parse_access_env(getenv("SANDBOX_ACCESS"));
    int rs_fd = create_landlock_ruleset(access);
    if (rs_fd < 0) return; // Landlock 不支持,静默降级

    // 从环境变量读取沙箱根目录
    const char *root = getenv("SANDBOX_ROOT");
    if (!root || add_directory_rule(rs_fd, root, access) != 0) {
        close(rs_fd);
        return;
    }

    // 可选:允许读取 /etc/ssl/certs(TLS 验证需要)
    const char *allow_ssl = getenv("SANDBOX_ALLOW_SSL");
    if (allow_ssl && strcmp(allow_ssl, "1") == 0) {
        add_directory_rule(rs_fd, "/etc/ssl/certs",
                           LANDLOCK_ACCESS_FS_READ_FILE |
                           LANDLOCK_ACCESS_FS_READ_DIR);
    }

    // 可选:允许读取 /dev/urandom 和 /dev/null(libc 需要)
    // 注意:Landlock 目前不控制设备文件访问,需要通过 bind mount + allow dir 实现
    // 实际生产中应结合 mount namespace

    syscall(SYS_landlock_restrict_self, rs_fd, 0);
    close(rs_fd);
}

编译和使用:

# 编译共享库
gcc -shared -fPIC -o libsandbox.so libsandbox.c -ldl

# 将 nginx 限制在 /var/www 和 /var/log/nginx 下运行
SANDBOX_ROOT=/var/www \
SANDBOX_ACCESS="READ_FILE,READ_DIR,WRITE_FILE" \
LD_PRELOAD=./libsandbox.so \
    nginx -g "daemon off;"

4.3 高级模式:多层嵌套规则

Landlock v1 只允许单层规则树,v2(内核 6.2+)引入了层次化规则——一个子目录的规则可以在父目录规则基础上进一步收紧:

// Landlock v2 API 示例——子目录进一步收紧父目录权限
// 在父规则允许读写的前提下,对 .ssh 目录只禁止写入
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 2, 0)
#include <linux/landlock.h>

// LANDLOCK_RULE_NET_PORT 是 v2 新增的网络规则类型
// LANDLOCK_ACCESS_FS_TRUNCATE 在 v3(内核 6.7+)加入

// 层次化规则的关键是 LANDLOCK_ADD_RULE_PATH_BENEATH 可以
// 嵌套到 ruleset 的 tree 结构中
#endif

五、Landlock 与现有安全机制的协同

Landlock 不是孤立的安全层,真正实现防御深度需要多层联动:

5.1 与 seccomp-bpf 的组合

seccomp 过滤系统调用,Landlock 感知文件语义,两者完全不重叠:

# Dockerfile 示例
FROM alpine:3.19
RUN apk add --no-cache libseccomp

# 应用 seccomp profile(只允许必要的 syscall)
COPY seccomp-profile.json /etc/seccomp/

# 应用 Landlock(只允许文件访问范围)
# 通过 entrypoint 脚本在容器内挂载
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
#!/bin/sh
# entrypoint.sh —— 在容器内以 root 启动,但在 spawn worker 前应用沙箱
# 步骤1:加载 seccomp profile

# 步骤2:使用 landlock-self-sandbox 工具应用 landlock
/usr/bin/landlock-sandbox \
    --allow /app \
    --allow /tmp \
    --allow /var/log/app \
    --drop-all \
    -- \
    exec su -s /bin/sh app -c "/usr/local/bin/myapp"

5.2 与 eBPF / LSM BPF 的关系

eBPF LSM(bpf-LSM)提供了可编程的 LSM hook 拦截能力,Landlock 也可以通过 eBPF 实现自定义策略:

┌────────────────────────────────────────────────┐
│               eBPF LSM BPF 程序                │
│  - 自定义安全策略逻辑                           │
│  - 动态策略更新(无需重启进程)                 │
│  - 可以访问网络、IPC 等多个 LSM hook           │
└────────────────────┬───────────────────────────┘
                     │ 策略决策
┌────────────────────┴───────────────────────────┐
│                  Landlock                       │
│  - 确定性策略(加载后不可变)                   │
│  - 可嵌套继承                                   │
│  - 轻量高效,决策复杂度 O(1) ~ O(log N)        │
└────────────────────────────────────────────────┘

选择原则: 如果你需要"加载后不可篡改"的确定性安全保证(例如金融场景的 WORM 合规要求),选 Landlock;如果你需要运行时动态更新策略(例如 SaaS 多租户隔离),eBPF LSM 更合适。

5.3 与 User Namespace 的配合

User Namespace 解决"权限身份"问题,Landlock 解决"文件系统访问范围"问题。正确的安全栈应同时部署:

// 1. unshare(CLONE_NEWUSER | CLONE_NEWNS) 进入新的 user + mount namespace
// 2. 在新 namespace 内 drop all capabilities
// 3. prctl(PR_SET_NO_NEW_PRIVS, 1)
// 4. landlock_restrict_self(ruleset)
// 5. 此时:root 权限被 namespace 消解,文件访问被 landlock 收敛
//    攻击面 = (namespace 逃逸) × (landlock 绕过) —— 概率乘积最低

六、性能与工程实践考量

6.1 性能基准

Landlock 的性能开销极低——它在 VFS 访问路径上挂载了 LSM hook,每个规则检查是 O(log N) 的 BST 查找(N 为规则数量):

规则数量    单次 check 耗时(典型 x86_64)
1 个规则     ~8 ns
10 个规则    ~15 ns
100 个规则   ~25 ns
1000 个规则 ~40 ns

对比:上下文切换约 1-3μs,缺页中断 1-5μs,系统调用本身 50-200 ns。Landlock 引入的额外延迟在系统调用总耗时中占比通常低于 5%。

6.2 规则数量限制

Landlock 默认最多支持 64 层规则嵌套(v2 提升到 2^16),每条 ruleset 的路径 entry 数受 RLIMIT_MEMLOCK 限制——每条路径规则缓存约 1-2KB 内核内存。

6.3 常见工程陷阱

陷阱 1:忘记 O_PATH——使用 O_RDONLY 等模式打开 parent_fd 会触发 access check,而此时 ruleset 还未应用,可能导致死锁或权限错误。

陷阱 2:符号链接跟随——Landlock 在规则匹配时是否跟随符号链接取决于内核版本:v1 不跟随,v2 可选。跨 mount boundary 的符号链接在大多数环境下不被跟随,需要预先为 target mount 添加相应的 rule。

陷阱 3:设备文件——Landlock 当前不拦截对设备文件(/dev/sda、/dev/mem 等)的 open(v1)。如果你的沙箱需要考虑设备访问,应配合 device cgroup 使用。

陷阱 4:LANDLOCK_ACCESS_FS_REFER——如果你允许 rename() 和 link(),但未显式设置 _REFER 权限,则跨目录移动文件失败。这是 v3(内核 6.7+)引入的新权限,也是容器内文件管理的关键控制点。


七、从原型到生产:完整部署架构

在 containerd + K8s 环境下,Landlock 的标准部署形态为:

Pod
├─ init container: 设置 namespace、加载 seccomp profile、预计算 ruleset
├─ app container:
│   ├─ entrypoint: 调用 landlock_self_sandbox(1) 应用文件系统沙箱
│   └─ worker: 在沙箱内启动实际应用
└─ sidecar (可选): eBPF 程序监控 landlock 触发的 EACCES 事件

对于自研容器运行时,可以通过 ociRuntimeSpec 的 linux.landlock 字段(OCI 1.1 草案即将标准化)将 Landlock 规则作为容器配置的一部分声明式传递:

{
  "linux": {
    "landlock": {
      "ruleset": {
        "handled_access": "fs_read_write_execute",
        "rules": [
          { "path": "/app/data", "access": "fs_read_write" },
          { "path": "/tmp", "access": "fs_read_write_make" },
          { "path": "/etc/ssl/certs", "access": "fs_read" }
        ]
      }
    }
  }
}

八、总结与展望

Landlock 的核心价值不是技术创新——它的设计非常保守和简洁——而是它在正确的时间填补了 Linux 安全模型中长期缺失的一环:非特权程序自主定义文件系统约束。

从内核 5.13 的初始版本到 6.8 的网络端口控制,Landlock 正在逐步扩大防护边界。2025-2026 年的重点方向包括:

  1. Landlock Network规则(已基本完成)——进程自主限制 bind() 和 connect() 的目标端口
  2. 更好的嵌套语义——vN 引入的规则树深度管理
  3. 与 io_uring 兼容——异步 IO 操作如何受 Landlock 约束仍在讨论中
  4. WASI 适配——将 Landlock 规则映射到 WASI capabilities model

在容器逃逸频发的当下,把 Landlock 作为纵深防御的最后一道关卡,是一项投入产出比极高的安全加固手段。对于运行用户提交代码的 SaaS 平台、多租户边缘计算节点、或任何需要"被隔离的代码可信度存疑"的场景,Landlock 都值得成为你的安全栈标准配置。


参考:Landlock ABI v1-v4 官方文档、Linux 6.8 security/landlock/ 源码、CVE-2024-21626 runc 漏洞分析报告

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.425727s