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 年的重点方向包括:
- Landlock Network规则(已基本完成)——进程自主限制
bind()和connect()的目标端口 - 更好的嵌套语义——vN 引入的规则树深度管理
- 与 io_uring 兼容——异步 IO 操作如何受 Landlock 约束仍在讨论中
- WASI 适配——将 Landlock 规则映射到 WASI capabilities model
在容器逃逸频发的当下,把 Landlock 作为纵深防御的最后一道关卡,是一项投入产出比极高的安全加固手段。对于运行用户提交代码的 SaaS 平台、多租户边缘计算节点、或任何需要"被隔离的代码可信度存疑"的场景,Landlock 都值得成为你的安全栈标准配置。
参考:Landlock ABI v1-v4 官方文档、Linux 6.8 security/landlock/ 源码、CVE-2024-21626 runc 漏洞分析报告

发表评论 取消回复