Linux 内核 Landlock LSM:无权限沙箱安全架构与深度工程实践

当你在浏览器中打开一个恶意 PDF 时,它凭什么不能读取你的 SSH 密钥?当你的 CI 脚本在容器里被植入后门时,凭什么不能执行 mount 和 ptrace?这些问题的答案,不在 DAC 权限位里,不在 Linux capabilities 的 40 个旗子里,而是在一个从零设计的、无权限的、堆叠式的安全模块中——Landlock。

Landlock 是 Linux 内核 5.13(2021 年 6 月)引入的 Security Module(LSM),它允许非特权进程在不需要任何 capability(如 CAP_SYS_ADMIN)的前提下,自行限制自己的行为。它诞生于对容器安全、应用沙箱和无服务场景下最小权限原则的深刻理解,与 SELinux 和 AppArmor 并行运行,但有着截然不同的设计哲学:谁使用,谁创建,谁继承。

一、为什么需要 Landlock:DAC 与 MAC 的裂缝

Linux 传统安全模型是一个多层体系:

  • DAC(自主访问控制):基于属主/组/其他三类权限位,以及 ACL。核心问题是:进程以用户身份运行,无法自我限制——一个非 root 进程虽然不能碰 root 的文件,但可以读写自己拥有的所有文件,包括 ~/.ssh/id_rsa 这样的人质文件。
  • Capabilities(能力分割):将 root 的超级权力拆成 40 多个独立 capability。但其粒度仍然粗放——CAP_DAC_OVERRIDE 可以绕过所有 DAC 检查;而真正的沙箱需要的是"只能读 /tmp/sandbox/ 而非系统全局"这种精确的时空约束。
  • SELinux / AppArmor:基于管理员下发的全局策略。容器和桌面应用需要的是应用级的、无需管理员的访问控制——一个用户级 PDF 阅读器不需要重启时询问系统管理员"我要不要把 ~/Documents 纳入沙箱"。
  • seccomp-bpf:限制系统调用,但无法基于文件路径做访问控制。

Landlock 恰好填补了这个裂缝:非特权进程对自身行为的细粒度、基于规则的限制,不需要任何外部策略配置。

二、Landlock 设计哲学:规则而非对象

Landlock 的核心创新是它的"规则即对象"模型。与 SELinux 打标签、seccomp 过滤 syscall 不同,Landlock 采用的是基于文件系统访问规则的程序化沙箱。

2.1 访问规则集(Access Rule Set)

Landlock 定义了一个 landlock_ruleset 的概念,包含一组"允许做"的规则——默认全部拒绝。规则可以约束的动作包括:

访问类型 含义
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 创建 socket
LANDLOCK_ACCESS_FS_MAKE_FIFO 创建 FIFO
LANDLOCK_ACCESS_FS_MAKE_BLOCK 创建块设备
LANDLOCK_ACCESS_FS_MAKE_SYM 创建符号链接
LANDLOCK_ACCESS_FS_REFER 跨目录硬链接/重命名(v1.0+)
LANDLOCK_ACCESS_FS_TRUNCATE 截断文件(v3.0+)
LANDLOCK_ACCESS_FS_IOCTL_DEV ioctl 设备(v6.2+)
LANDLOCK_ACCESS_NET_BIND_TCP 绑定 TCP 端口(v6.7+)
LANDLOCK_ACCESS_NET_CONNECT_TCP 连接 TCP(v6.7+)

2.2 ABI 版本演进

Landlock 是一个稳定 ABI:内核只新增访问标志位,旧程序不受影响。ABI 版本号从 1 递增:

内核版本 ABI 版本 新增能力
5.13 v1 文件系统基础规则
5.19 v2 LANDLOCK_FS_TRUNCATE
6.2 v3 LANDLOCK_FS_IOCTL_DEV
6.7 v4 网络(BIND/CONNECT TCP)
6.11 v5 LANDLOCK_FS_REFER 扩展
6.13 v6 LANDLOCK_FS_TRUNCATE 统一

这意味着一个编译时针对 v6 ABI 的程序,在 v4 内核上仍能正常工作——只是无法使用 v5/v6 引入的新特性。Landlock 通过 landlock_create_ruleset() 的 attr 字段中的 handled_access_fs 和 handled_access_net 位掩码来声明能力集。

三、Landlock 系统调用链路

Landlock 通过一组自定义系统调用与内核交互。虽然提供了 liblandlock 用户态封装,但了解底层 ABI 有助于理解其约束条件。

3.1 核心 syscall 序列

landlock_create_ruleset()  → 创建规则集(包含要允许的访问掩码)
      ↓
landlock_add_rule()       → 向规则集添加规则(路径 + 子集访问掩码)
      ↓
landlock_restrict_self()  → 将规则集应用到当前进程及其子进程
      ↓
prctl(PR_SET_NO_NEW_PRIVS, 1) → 前置条件:禁止提权

关键点:prctl(PR_SET_NO_NEW_PRIVS, 1) 是强制前置条件。它确保沙箱一旦设置,子进程(即使是 setuid 二进制)也无法提升权限退出沙箱。这正是 Landlock 能作为安全边界使用的核心保证——它不是"建议性"限制,而是内核级别、不可绕过的强制访问控制。

3.2 路径匹配语义

Landlock 对路径的匹配并非简单的字符串前缀。内核 v5.13 起只支持目录层级匹配,规则的 pathname 必须是一个目录路径(不一定是挂载点),内核将对这个目录及其所有子路径递归应用这条规则。路径不能是普通文件、符号链接或挂载点的父路径之外的位置。

V1.0 中有一个重要限制:规则只能降权,不能越限。子进程创建的 ruleset 中,每条规则的权利集合必须是父进程活动集合的子集。

四、从零实现一个 Landlock 沙箱

下面通过一个完整的 C 示例,演示如何用 Landlock 构建一个"只能写目录、只能读目录"的最小权限文件处理环境。

4.1 compile 与依赖

# 需要 liblandlock(通常由 linux-headers + libseccomp 环境提供)
# Dev 包:apt install liblandlock-dev 或 dnf install liblandlock-devel

gcc -o landlock_sandbox landlock_sandbox.c -llandlock

4.2 完整源码

// landlock_sandbox.c — 最小化 Landlock 文件系统沙箱演示
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#ifndef landlock_create_ruleset
static int
landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
                        size_t size, __u32 flags)
{
    return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
#endif

#ifndef landlock_add_rule
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type type,
                             const void *attr, __u32 flags)
{
    return syscall(__NR_landlock_add_rule, ruleset_fd, type, attr, flags);
}
#endif

#ifndef landlock_restrict_self
static int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
    return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}

// 我们允许的访问权限
#define ACCESS_FS_READ  (LANDLOCK_ACCESS_FS_READ_FILE | \
                         LANDLOCK_ACCESS_FS_READ_DIR)
#define ACCESS_FS_WRITE (LANDLOCK_ACCESS_FS_WRITE_FILE | \
                         LANDLOCK_ACCESS_FS_MAKE_REG | \
                         LANDLOCK_ACCESS_FS_REMOVE_FILE)
#define ACCESS_FS_EXEC  LANDLOCK_ACCESS_FS_EXECUTE

static int add_path(int ruleset_fd, const char *path, __u32 access)
{
    struct landlock_path_beneath_attr path_attr = {
        .allowed_access = access,
        .parent_fd = open(path, O_PATH | O_CLOEXEC),
    };
    if (path_attr.parent_fd < 0) {
        fprintf(stderr, "open '%s': %s\n", path, strerror(errno));
        return -1;
    }
    if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                          &path_attr, 0)) {
        fprintf(stderr, "landlock_add_rule '%s': %s\n", path, strerror(errno));
        close(path_attr.parent_fd);
        return -1;
    }
    close(path_attr.parent_fd);
    return 0;
}

int main(int argc, char **argv)
{
    struct landlock_ruleset_attr ruleset_attr = {
        .handled_access_fs = ACCESS_FS_READ | ACCESS_FS_WRITE | ACCESS_FS_EXEC,
    };
    int ruleset_fd;

    // 前置:禁止新权限——永不回退
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        return EXIT_FAILURE;
    }

    ruleset_fd = landlock_create_ruleset(&ruleset_attr,
                                          sizeof(ruleset_attr), 0);
    if (ruleset_fd < 0) {
        perror("landlock_create_ruleset");
        fprintf(stderr, "当前内核可能不支持 Landlock(需 5.13+)\n");
        return EXIT_FAILURE;
    }

    // 规则 1:/tmp/sandbox/ 可读写
    if (add_path(ruleset_fd, "/tmp/sandbox",
                 ACCESS_FS_READ | ACCESS_FS_WRITE))
        return EXIT_FAILURE;

    // 规则 2:/usr/lib、/usr/bin 只读(执行库和工具)
    if (add_path(ruleset_fd, "/usr/lib", ACCESS_FS_READ))
        return EXIT_FAILURE;
    if (add_path(ruleset_fd, "/usr/bin", ACCESS_FS_READ | LANDLOCK_ACCESS_FS_EXECUTE))
        return EXIT_FAILURE;

    // 规则 3:/etc/ssl 只读(TLS 需要)
    if (add_path(ruleset_fd, "/etc/ssl", ACCESS_FS_READ))
        return EXIT_FAILURE;

    // 应用规则集到当前进程和此后所有子进程
    if (landlock_restrict_self(ruleset_fd, 0)) {
        perror("landlock_restrict_self");
        return EXIT_FAILURE;
    }
    close(ruleset_fd);

    printf("[sandbox] Landlock 已生效。尝试访问受限路径...\n");

    // 测试:在允许的目录写
    int fd = open("/tmp/sandbox/test.txt", O_CREAT | O_WRONLY, 0644);
    if (fd >= 0) {
        write(fd, "sandbox works\n", 14);
        close(fd);
        printf("[✓] 写入 /tmp/sandbox/ 成功\n");
    }

    // 测试:在拒绝的目录写(预期失败)
    fd = open("/tmp/evil.txt", O_CREAT | O_WRONLY, 0644);
    if (fd < 0)
        printf("[✓] 拒绝写入 /tmp/ 符合预期: %s\n", strerror(errno));
    else
        fprintf(stderr, "[✗] 意外成功!安全边界失效\n");

    return EXIT_FAILURE;
}

编译执行后输出:

[sandbox] Landlock 已生效。尝试访问受限路径...
[✓] 写入 /tmp/sandbox/ 成功
[✓] 拒绝写入 /tmp/ 符合预期: Operation not permitted

Operation not permitted (EPERM) 是 Landlock 拒绝访问时的返回码——与 DAC 权限不足相同,符合 POSIX 语义。

4.3 Landlock 的"不透明"规则

一个设计精巧之处:沙箱内的进程无法通过 /proc/self/mountinfo 或 /sys 判断哪些路径有 Landlock 规则。规则也不通过 /proc//attr/current 暴露给管理员(与 SELinux 不同)。只有 root + CAP_SYS_ADMIN 进程可以通过特定的 Landlock 诊断接口读取规则(未来版本)。

这意味着即便是 root 也无法轻易审计一个进程的 Landlock 边界,除非它主动暴露。这是深思熟虑的隐私设计——systemd 不会恶意规避它的服务沙箱规则。

五、网络规则(ABI v4+):TCP BIND/CONNECT 控制

内核 6.7 起 Landlock 扩展支持网络访问控制,仅涵盖 TCP:

// 在网络规则集初始化时声明处理能力
struct landlock_ruleset_attr attr = {
    .handled_access_fs  = ACCESS_FS_ALL,
    .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
                          LANDLOCK_ACCESS_NET_CONNECT_TCP,
};

// 绑定到 8080 端口
struct landlock_net_port_attr net_attr = {
    .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
    .port = 8080,
};
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, &net_attr, 0);

网络规则的粒度是 port 级别:你可以控制"进程只能绑定哪些端口"和"进程只能连接到哪些端口",但不能限制目标 IP 或做 L7 内容过滤。这是与文件系统设计一致的"简单但覆盖关键"思路——阻止一个恶意服务绑定到 22 端口,防止它冒充 SSH 守护进程。

六、生产级实战场景

6.1 浏览器渲染进程沙箱

Chromium 在 Linux 桌面平台上使用 Landlock 补充其 seccomp-bpf 沙箱。渲染进程(Renderer)在解析 HTML/CSS/JS 时需要:

  • 读取 /sys 下的 CPU/GPU 信息用于 WebGPU/WebGL
  • 读取 ~/.config 下少量字体配置
  • 写入 /tmp 下的缓存文件
  • 绝对禁止写入 ~/.ssh、~/.gnupg 等敏感位置

Landlock 的规则文件层级使这种需求变为简单的"路径组"配置文件。相比 seccomp 需要的庞大 BPF 过滤链,Landlock 的规则声明更接近安全工程师的直觉。

6.2 systemd 服务降权

systemd 254+ 通过 RestrictFileSystems= 原生集成 Landlock。一个服务的 unit 文件可以这样声明:

[Service]
RestrictFileSystems=:/var/log/myapp  # 读写此目录
                   /usr/share/myapp   # 只读数据
                   ~/static           # 只读家目录

# 等价于传统:ReadOnlyPaths= + ReadWritePaths= + InaccessiblePaths=
# 但Landlock优势:无需CAP_SYS_ADMIN,无需结合其它服务配置相互作用

这家意味着即便服务被 RCE 攻击,攻击者也无法读取 /etc/shadow、写 crontab 或修改二进制文件。

6.3 CI 脚本执行隔离

在 CI runner(GitHub Actions、GitLab Runner)中,用户脚本以 runner UID 运行。结合 Landlock:

  • 脚本只能写入 $CI_PROJECT_DIR
  • 不能读取其他项目的缓存目录
  • 不能调用危险设备 /dev/sda 的 ioctl

一个攻击者注入的恶意 PR 脚本,在 Landlock 下会被限制在项目目录内,无法横向窃密。

6.4 容器内二次沙箱

Docker/K8s 容器默认配置下,容器内 root 实际上拥有 CAP_SYS_ADMIN 以外的所有 capability,且可以执行 mount、创建设备节点、修改 cgroup。在容器内再叠加一层 Landlock 可以实现纵深防御:

  • 容器内的应用以非 root(UID 1000)运行
  • Landlock 限制应用只能读写 /app/data
  • 即便应用逃逸出 namespace(假设漏洞存在),主机内核仍会拒绝它访问 /proc/1/root 中的主机文件

这在内核态完成了 namespace 逃逸后的二次拦截,且不需要任何主机端配置。

七、性能与内核实现

Landlock 的性能设计遵循零开销原则:没有规则就没有成本。

7.1 LSM Hook 注入

Landlock 通过 LSM(Linux Security Modules)在四个关键 hook 点插入检查:

  • file_open / file_permission:文件读写执行路径
  • file_truncate:截断文件(使 TRUNCATE 访问标志生效)
  • path_link / path_rename / path_mkdir / path_mknod 等:路径操作
  • socket_bind / socket_connect:网络控制(v4+)

每次 Landlock kernel 检查的成本是 O(文件路径规则数)。对于典型桌面/服务场景(10-100 条路径规则),单次查找的 CPU 耗时在 <100ns,远低于 syscall 和 VFS 自身开销。

7.2 规则存储结构

内核使用一个 per-thread(实际上是 per-Cred)的规则树(rule_domain),每条规则以红黑树按路径层级组织。当 Landlock allow_access() 被调用时:

文件打开 → security_file_open()
    → cred->security 中查找 Landlock ruleset
    → 从 dentry 向上遍历父路径
    → 在 ruleset 的红黑树中查找匹配的规则
    → 命中则允许,未命中则拒绝(EPERM)

这个"向上遍历"机制是 Landlock 支持真正的目录层级访问 的核心——不需要为每层目录预先创建规则。

7.3 与 eBPF 的关系

Landlock 本身不使用 eBPF 字节码——规则在创建时就固定下来了。但这不意味着二者不能配合:eBPF 程序可以挂钩 LSM hook(如 bpf_lsm),与 Landlock 共同守卫同一批操作。区别在于 eBPF 能做动态策略和审计,而 Landlock 只做静态且最终拒绝的硬规则。

八、ABI 兼容实践

编译 ABI v6 的程序在 v4 内核上运行会迎来一个关键行为:降级运行。liblandlock 在 landlock_create_ruleset() 时传入的能力集会被内核裁剪到当前 ABI 支持的最大值。

struct landlock_ruleset_attr attr = {
    .handled_access_fs = LANDLOCK_ACCESS_FS_REFER |  // v5+
                          LANDLOCK_ACCESS_FS_TRUNCATE, // v2+
};

// 在 v4 内核上,LANDLOCK_ACCESS_FS_REFER 忽略不报错误
// 在 v2 内核上,LANDLOCK_ACCESS_FS_TRUNCATE 忽略
// 只有两个位都不支持时,create_ruleset 返回 -EINVAL

这意味着:

  1. 安全设计:在降级运行时,越权的访问操作会被拒绝,不会意外地"全局放行"。
  2. 部署策略:应用应在初始化时检测内核 ABI 版本,决定使用哪些规则。liblandlock 提供了 landlock_abi_version() 来查询。

九、Landlock 当前局限与未来方向

9.1 已知限制

  • 仅目录层级匹配:不能对单个文件设置规则,只能对"某目录及所有子路径"设置。这迫使你将需要不同权限的文件分布在不同目录下。
  • 不支持挂载操作:不能限制 mount()——这是 /proc/sys/fs/binfmt_misc、FUSE 等复杂文件系统的安全边界挑战。
  • 网络仅 TCP:UDP、ICMP、Unix socket 等不在 v4 范围内。未来 ABI 版本可能增加。
  • 规则静态:创建后不能动态增删规则——需要创建新的 ruleset 并 restrict_self() 应用更严格的规则集(只能收紧,不能放松)。
  • 无审计日志:被 Landlock 拒绝的操作不会产生 audit_log(部分内核版本通过 audit_context 有限支持)。

9.2 上游发展方向

根据 Linux 安全模块子系统的讨论,Landlock 近期可能扩展:

  1. 网络协议扩展:UDP bind/connect 控制(已在部分开发分支)
  2. 套接字创建控制:限制 socket domain/protocol
  3. 审计集成:拒绝事件写入 dmesg/audit
  4. BPF 扩展:允许对 Landlock hook 点加载 eBPF 程序做动态策略(已在 LSM BPF 框架内讨论)

十、总结

Landlock 代表了一种新的安全模块范式:用户态驱动、自我约束、无特权、默认拒绝。它既不替代 SELinux(面向管理员的全局策略),也不替代 seccomp(系统调用级别限制)——它正好在这个光谱上找到了自己的精确位置。

在生产环境中,Landlock 的实际价值来源于三个保证:

  1. 不可绕过性:PR_SET_NO_NEW_PRIVS 确保沙箱无法通过 setuid 等机制逃逸。
  2. 堆叠性:与既有能力、DAC、SELinux、seccomp 并行生效,取交集(AND)。
  3. 零依赖:不需要策略文件分发,不需要 root 管理员参与,不需要重启主机。

当一个安全机制的信任链短到"只需编译进应用本身,随应用分发"时,它的实际覆盖率和采用率远远超过那些依赖运维配合的安全方案。Landlock 正在沿着这条路走向未来——看看 systemd、Chrome、systemd/resolved 和越来越多的 EDA 工具采用它,就知道了。

// 极简 poc——两行核心代码
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);  // 拒绝提权
landlock_restrict_self(
    landlock_create_ruleset(&(typeof(struct landlock_ruleset_attr)){
        .handled_access_fs = ACCESS_FS_READ_FILE | ACCESS_FS_WRITE_FILE
    }, sizeof(attr), 0), 0
);
// 此后的进程,能写的文件只能是你明确允许的那些。

Landlock 不会阻止所有攻击。但它确保了 被攻破的进程,依然是沙箱里的进程——这是现代 Linux 安全架构中最缺少的一环。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部