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/ 暴露给管理员(与 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
这意味着:
- 安全设计:在降级运行时,越权的访问操作会被拒绝,不会意外地"全局放行"。
- 部署策略:应用应在初始化时检测内核 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 近期可能扩展:
- 网络协议扩展:UDP bind/connect 控制(已在部分开发分支)
- 套接字创建控制:限制 socket domain/protocol
- 审计集成:拒绝事件写入 dmesg/audit
- BPF 扩展:允许对 Landlock hook 点加载 eBPF 程序做动态策略(已在 LSM BPF 框架内讨论)
十、总结
Landlock 代表了一种新的安全模块范式:用户态驱动、自我约束、无特权、默认拒绝。它既不替代 SELinux(面向管理员的全局策略),也不替代 seccomp(系统调用级别限制)——它正好在这个光谱上找到了自己的精确位置。
在生产环境中,Landlock 的实际价值来源于三个保证:
- 不可绕过性:
PR_SET_NO_NEW_PRIVS确保沙箱无法通过 setuid 等机制逃逸。 - 堆叠性:与既有能力、DAC、SELinux、seccomp 并行生效,取交集(AND)。
- 零依赖:不需要策略文件分发,不需要 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 安全架构中最缺少的一环。

发表评论 取消回复