Linux 上给普通进程做沙箱,一直不算顺手。seccomp-bpf 能限制系统调用,但得自己写 BPF 程序,端口号变了还得改过滤器。容器太笨重,bubblewrap 依赖 setuid 辅助。Firejail 倒是好用,但它的 setuid 二进制本身就是攻击面。Landlock 是内核直接提供的一条路:不需要 root,不需要 setuid,也不需要写 BPF。它在 5.13 合入主线,但使用率还不高。
背景#
Landlock 是 Mickaël Salaün 在 2021 年合入主线的 Linux Security Module(LSM)。它要解决的问题很明确:允许任意进程,包括非特权进程,主动收窄自己的资源访问范围。注意“主动”这个词。Landlock 不是 SELinux/AppArmor 那类强制 MAC 策略,而是 self-sandboxing。
传统沙箱方案的问题:
| 方案 | 需要 root/setuid | 优势 | 硬伤 |
|---|---|---|---|
| 容器 (Docker/Podman) | 需要(daemon root) | 完整的命名空间隔离 | 启动慢,镜像大,资源重 |
| seccomp-bpf | 不需要(prctl) | 精细控制系统调用 | BPF 过滤器维护成本高 |
| bubblewrap | 需要 setuid | 轻量命名空间 | setuid 二进制是攻击面 |
| Firejail | 需要 setuid | 预置 1000+ profile | 同样依赖 setuid |
| Landlock | 不需要 | 零特权沙箱,纯系统调用 API | 仅限文件/网络,不隔离进程视图 |
Landlock 不替代 seccomp 或容器,它补的是中间那块空白:普通进程自己给自己加限制,不依赖外部工具。写好规则集,调用三个系统调用,然后权限就收紧。
工作机制#
Landlock 的 API 很小,只有三个系统调用:
| |
锁住之后不可逆。没有"解除"的接口。这是设计上的故意选择:一旦你限制了自己,同一个进程无法放宽限制,只能被新线程继承。
规则类型(截至 ABI v6)#
Landlock 的 ABI 版本在演进,每个版本追加新的能力:
| ABI | Linux 内核 | 新能力 |
|---|---|---|
| 1 | 5.13 | 基本文件系统限制(读写执行) |
| 2 | 5.19 | LANDLOCK_ACCESS_FS_REFER(跨目录 rename/link) |
| 3 | 6.2 | LANDLOCK_ACCESS_FS_TRUNCATE(截断文件) |
| 4 | 6.7 | 网络限制(TCP bind/connect) |
| 5 | 6.10 | LANDLOCK_ACCESS_FS_IOCTL_DEV(ioctl 限制) |
| 6 | 6.12 | IPC 作用域(abstract UNIX socket + 信号隔离) |
当前最新是 ABI v6(Linux 6.12+,2024 年底)。每个版本向后兼容。你在老内核上降级能力就行。
分层模型#
Landlock 使用多层策略(layers)。每次调用 landlock_restrict_self 追加一层,最多 16 层。访问检查通过所有层的规则才放行。跟 SELinux 的 intersection 逻辑一样,不能的权限一层否决就能挡住。
| |
锁住之前必须调用 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)。这个操作会防止进程通过 setuid、setcap 等方式获得新特权。Landlock 要求你不能一边限制自己一边偷偷升级。
代码示例#
下面这个 C 程序把进程限制在“只读 /usr + 读写 /tmp + 能连 GitHub HTTPS”的规则内。
| |
编译运行:
| |
行为对比(同样调 open("/etc/shadow", O_RDONLY)):
| 操作 | 无沙箱 | 有沙箱 |
|---|---|---|
读 /etc/shadow | 普通用户→拒绝(DAC) | 拒绝(Landlock 层先拦住) |
读 /usr/bin/gcc | 允许 | 允许(规则 A) |
写 /tmp/test.txt | 允许 | 允许(规则 B) |
| 连 example.com:80 | 允许 | 拒绝(没声明 80 端口) |
| 连 github.com:443 | 允许 | 允许(网络规则) |
/etc/shadow 本来就会被 DAC 拦住普通用户读取。Landlock 的价值在另一层:如果 DAC 配错了,比如权限被改成 644,Landlock 仍然会挡住。
关于 ABI 兼容#
写产品级代码时一定要做 ABI 降级。上面的示例用了简单的 switch,更健壮的做法是:
| |
不这样做的话,在旧内核上 landlock_create_ruleset 会返回 EINVAL 或 ENOSYS。
使用现状#
已经有几个项目在生产环境用 Landlock:
| 项目 | 用途 | 备注 |
|---|---|---|
| landrun | 通用 CLI 沙箱工具 | Go 实现,2025 年火过一波 |
| nono | 无特权能力限制引擎 | 生产级使用 |
| systemd | service 的 RestrictFileSystems= | 从 v255 开始实验支持 |
内核 samples/landlock/sandboxer.c | 官方参考实现 | 功能完整的沙箱管理器 |
| go-landlock | Go 语言的 Landlock 绑定 | 官方维护 |
| Firejail (待合并) | 已有 issue #5269 提议加入 | 还没合入 |
Landrun 值得看一下,用法很直接:
| |
主要逻辑就是封装了上面那段 C 代码。跟 Firejail 的区别是:landrun 不需要 setuid,不需要安装 daemon,不修改 /proc/sys 配置。就是一个普通的 Go 二进制,任何用户都能跑。
可以马上做的事#
检查内核是否支持 Landlock:运行
dmesg | grep landlock或journalctl -kb -g landlock。如果没输出,检查/boot/config-*中CONFIG_SECURITY_LANDLOCK=y。Debian/Ubuntu 的通用内核默认启用。Arch Linux 也是。跑官方的 sandboxer 示例:在 Linux 内核源码树里执行以下操作:
1 2 3 4git clone --depth=1 https://github.com/torvalds/linux.git /tmp/linux make -C /tmp/linux samples/landlock/sandboxer LL_FS_RO="/usr:/lib:/etc/ssl" LL_FS_RW="/tmp" \ LL_TCP_CONNECT="443:80" ./samples/landlock/sandboxer bash试试在沙箱里
rm /etc/hostname。对比外面和里面的效果。试用 landrun:
go install github.com/Zouuup/landrun@latest,然后:1 2 3 4# 沙箱里跑 curl —— 只允许连 443 端口 landrun --ro /usr:/etc --net-connect 443 -- curl https://github.com # 试试连 80 端口,应该被拒绝 landrun --ro /usr:/etc --net-connect 443 -- curl http://example.com
参考#
- Landlock 官网和文档: https://landlock.io/
- landlock(7) 手册页: https://man7.org/linux/man-pages/man7/landlock.7.html
- 内核用户空间 API 文档: https://docs.kernel.org/userspace-api/landlock.html
- 内核 LSM 文档: https://www.kernel.org/doc/html/v5.13/security/landlock.html
- 官方示例 sandboxer.c: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/samples/landlock/sandboxer.c
- landrun GitHub: https://github.com/Zouuup/landrun
- LWN 文章 “Landlock LSM: unprivileged sandboxing”: https://lwn.net/Articles/840419/

