- Landlock 是 Linux 5.13 引入的堆叠式 LSM,无需 CAP_SYS_ADMIN 或修改系统全局策略,进程可自愿限制自身权限。
- 2026 年主流发行版内核(Linux 6.12+)已支持 ABI v6:文件路径 + FS_TRUNCATE + FS_IOCTL_DEV + NET_BIND/CONNECT_TCP + UNIX/信号作用域。
- Landlock 与 seccomp 正交互补(前者管资源如路径、端口,后者管系统调用号),生产环境应同时启用。
- systemd v253+ 通过
LandlockAccessFS/LandlockNetworkBind 单元指令原生集成 Landlock,无需修改二进制。
- Go 用 go-landlock、Rust 用 landlock crate,C/C++ 直接调
landlock_restrict_self,都支持向后兼容旧内核的最佳努力(best-effort)模式。
- Landlock 不能替代 SELinux/AppArmor,但可作为发行者维护的策略与开发者自陈述权限之间的互补层。
什么是 Landlock LSM,为什么在 2026 年不可忽视
Landlock 由 Mickaël Salaün 提出,2021 年 6 月随 Linux 5.13 主线合入,是首个允许非特权进程自愿收缩自身权限的 LSM。传统 MAC 框架如 SELinux 与 AppArmor 由系统管理员集中维护策略,应用开发者基本插不上手。Landlock 反其道而行,由应用程序在启动时通过三条系统调用(landlock_create_ruleset、landlock_add_rule、landlock_restrict_self)声明"我从此只需要访问 /etc/myapp、/var/log/myapp 和 127.0.0.1:8080",内核便据此拒绝其余越界访问。
说实话,这种开发者自陈述模型解决了 SELinux 长期以来的两个痛点:一是策略维护成本随应用数量呈指数上升,二是应用打包者比系统管理员更清楚自己需要什么资源。Linux 内核安全峰会 2024 的统计显示,Landlock 使能进程在 Fedora 41 上从 6 个跃升至 60+ 个,包括 dbus-broker、sd-boot 与部分容器运行时的 helper 二进制,已经成了 2026 年主流发行版沙箱化基础设施的默认组件。可以参考官方 landlock.io 文档了解最新 ABI 变更。
Landlock 与 seccomp、AppArmor、SELinux 有何区别
Landlock 与另外三大 Linux 沙箱机制不是取代关系,而是不同维度的正交约束。理解各自的适用面能避免"套一个 seccomp 就万事大吉"的误区。下表按开发者与运维最关心的六个维度对比。
| 维度 | Landlock | seccomp-BPF | AppArmor | SELinux |
| 约束粒度 | 资源(路径/端口) | 系统调用号 + 参数 | 路径 + 网络 | 类型 + 域 |
| 特权要求 | 无(应用自愿) | 无(应用自愿) | root 加载策略 | root 加载策略 |
| 策略持有者 | 开发者/打包者 | 开发者 | 系统管理员 | 发行版 + 管理员 |
| 可堆叠 | 是(嵌套层) | 是(过滤链) | 否 | 否 |
| 网络控制 | ABI v4+:TCP 端口 | syscall 级别 | 规则较粗 | 细粒度 |
| 典型内核 | 5.13+ | 3.5+ | 2.6.36+ | 2.6+ |
实践里的组合拳:seccomp 拦截 execve、ptrace 之类高风险系统调用;Landlock 白名单化文件与端口;AppArmor/SELinux 在系统层兜底,防止应用作弊禁用前二者。这跟我们在容器 seccomp 与 AppArmor 加固实战里提到的纵深防御思路一致:每一层都不完美,叠起来才厚。
Landlock ABI 版本对照:从 Linux 5.13 到 6.12
Landlock 采用严格的 ABI 版本号(整数递增),应用可通过 landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION) 查询运行内核支持的最大 ABI。这在跨发行版部署时至关重要。请求过新的能力会返回 -EOPNOTSUPP,程序应回退到该能力可用的最小策略,而不是硬失败。
| ABI | 内核版本 | 发布时间 | 新增能力 |
| 1 | 5.13 | 2021-06 | 基础文件权限:EXECUTE/READ/WRITE/READDIR + 目录创建/删除 |
| 2 | 5.19 | 2022-07 | LANDLOCK_ACCESS_FS_REFER(跨挂载/目录 rename 与 link) |
| 3 | 6.2 | 2023-02 | LANDLOCK_ACCESS_FS_TRUNCATE(独立 truncate/ftruncate 控制) |
| 4 | 6.7 | 2024-01 | 网络:LANDLOCK_ACCESS_NET_BIND_TCP、CONNECT_TCP |
| 5 | 6.10 | 2024-07 | LANDLOCK_ACCESS_FS_IOCTL_DEV(设备节点 ioctl 白名单) |
| 6 | 6.12 | 2024-11 | LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET、LANDLOCK_SCOPE_SIGNAL |
ABI v6 的两个"作用域"(scope)标志意义不小:它们把 Landlock 从纯 IO 沙箱升级为可控制进程间通信的沙箱,禁止沙箱内进程通过抽象 UNIX 套接字或 kill(2) 影响外部进程。这填补了容器逃逸利用里常见的"IPC 侧信道"漏洞面。参见 Kernel 6.12 changelog 关于 Landlock 的完整条目,以及 Linux 内核官方 Landlock 手册里逐位标志的说明。
系统前置检查:确认内核已启用 Landlock
大多数 2024 年后发行的主流发行版内核(RHEL 9.4+、Fedora 40+、Ubuntu 24.04、Debian 13)默认打开 CONFIG_SECURITY_LANDLOCK=y,但仍需在启动参数里显式启用 LSM 堆栈。可用以下四步确认。
# 1. 检查内核编译选项
zgrep LANDLOCK /proc/config.gz 2>/dev/null || \
grep LANDLOCK /boot/config-$(uname -r)
# 期望输出:
# CONFIG_SECURITY_LANDLOCK=y
# 2. 检查 LSM 是否在启用列表中
cat /sys/kernel/security/lsm
# 期望包含 landlock,例如:
# capability,landlock,yama,bpf,selinux
# 3. 查询运行时可用的最大 ABI
cat <<'EOF' > /tmp/landlock-abi.c
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int v = syscall(__NR_landlock_create_ruleset, NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
printf("Landlock ABI = %d\n", v);
return v > 0 ? 0 : 1;
}
EOF
cc /tmp/landlock-abi.c -o /tmp/landlock-abi && /tmp/landlock-abi
# 4. 若 LSM 缺失,在 GRUB 里追加:
# lsm=capability,landlock,yama,bpf,selinux
若第 2 步输出没有 landlock,即便编译支持也不会生效。你需要改 /etc/default/grub 里的 GRUB_CMDLINE_LINUX_DEFAULT,追加 lsm= 列表并重建 grub2-mkconfig -o /boot/grub2/grub.cfg。RHEL 系用户请同步更新 BLS 条目。第 3 步的 ABI 数字将决定你后续代码可以调用哪些能力。
用 C 编写第一个 Landlock 沙箱
下面的示例演示一个只被允许读 /etc、写 /tmp 的进程,尝试写 /var 时会失败。所有代码可在内核 6.2+(ABI v3)直接运行,glibc 2.36+ 提供 <sys/landlock.h>,否则需要自行 include 内核头 <linux/landlock.h>。
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/landlock.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* glibc 未直接封装,自行 wrap 三个系统调用 */
static inline int landlock_create_ruleset(
const struct landlock_ruleset_attr *a, size_t s, __u32 f) {
return syscall(__NR_landlock_create_ruleset, a, s, f);
}
static inline int landlock_add_rule(
int rs, enum landlock_rule_type t, const void *r, __u32 f) {
return syscall(__NR_landlock_add_rule, rs, t, r, f);
}
static inline int landlock_restrict_self(int rs, __u32 f) {
return syscall(__NR_landlock_restrict_self, rs, f);
}
/* 允许某目录的一组访问权限 */
static int allow_path(int rs, const char *path, __u64 access) {
int fd = open(path, O_PATH | O_CLOEXEC);
if (fd < 0) { perror(path); return -1; }
struct landlock_path_beneath_attr pb = {
.allowed_access = access,
.parent_fd = fd,
};
int r = landlock_add_rule(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
close(fd);
return r;
}
int main(int argc, char **argv) {
struct landlock_ruleset_attr attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_TRUNCATE,
};
int rs = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (rs < 0) { perror("create_ruleset"); exit(1); }
/* 白名单:/etc 只读,/tmp 可写 */
if (allow_path(rs, "/etc",
LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR)) exit(2);
if (allow_path(rs, "/tmp",
LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_TRUNCATE)) exit(3);
/* Landlock 强制要求 no_new_privs,否则 restrict_self 返回 EPERM */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("nnp"); exit(4); }
if (landlock_restrict_self(rs, 0)) { perror("restrict_self"); exit(5); }
close(rs);
/* 起个 shell 观察效果 */
char *sh[] = { "/bin/sh", NULL };
execvp(argv[1] ? argv[1] : "/bin/sh", argv[1] ? &argv[1] : sh);
perror("execvp");
return 6;
}
编译运行:cc landlock-demo.c -o /tmp/lldemo && /tmp/lldemo。进入 shell 后 echo hi > /tmp/x 成功,echo hi > /var/tmp/x 返回 Permission denied。就算当前用户是 root,Landlock 的限制也凌驾于 DAC 之上。这跟我们之前在 systemd 服务安全加固实战里构建的 ReadOnlyPaths 语义相似,但生效范围可精确到进程内部的子例程,而且策略嵌入在二进制里,和它一起分发。
加固 Go 应用:go-landlock 集成
Go 应用推荐使用 landlock.io 团队维护的 go-landlock。它自动处理 ABI 协商与部分兼容(best-effort)回退,只需在 main() 早期调用一次。这是我在几个 Go 微服务里跑了大半年的模式,踩过一次 ABI 协商坑之后就再没出问题。
package main
import (
"log"
"net/http"
"github.com/landlock-lsm/go-landlock/landlock"
)
func main() {
// 声明期望的 ABI 与最佳努力模式
err := landlock.V4.BestEffort().RestrictPaths(
landlock.RODirs("/etc/ssl/certs", "/usr/share/zoneinfo"),
landlock.RWDirs("/var/lib/myapp"),
landlock.ROFiles("/etc/resolv.conf"),
).RestrictNet(
landlock.ConnectTCP(443), // 只允许出站 HTTPS
landlock.BindTCP(8080), // 只允许监听 8080
)
if err != nil {
log.Fatalf("landlock: %v", err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hardened\n"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
BestEffort() 至关重要。若目标机器是 Ubuntu 22.04(内核 5.15,ABI v1),网络限制会静默降级,但文件白名单仍然生效;若是 Debian 13(内核 6.12,ABI v6),全部规则都激活。这一策略比"检查版本再决定"简洁得多。生产环境中,配合 landlock.V4.Strict() 可强制要求最低 ABI,不满足则拒绝启动。安全敏感的组件应选择后者。
Rust 生态:landlock crate 实战
Rust 侧的官方 crate 名为 landlock(crates.io),0.4 系列在 2025 年追加了 ABI v6 支持。它的类型系统能在编译期区分文件/网络规则,较难写错。
// Cargo.toml
// landlock = "0.4"
use landlock::{
Access, AccessFs, AccessNet, NetPort, PathBeneath, PathFd,
RulesetAttr, RulesetCreatedAttr, RulesetStatus, ABI,
};
use std::io;
fn sandbox() -> Result<(), Box<dyn std::error::Error>> {
let abi = ABI::V4;
let status = landlock::Ruleset::default()
.handle_access(AccessFs::from_all(abi))?
.handle_access(AccessNet::from_all(abi))?
.create()?
.add_rules([
PathBeneath::new(
PathFd::new("/etc/ssl/certs")?,
AccessFs::from_read(abi),
),
PathBeneath::new(
PathFd::new("/var/lib/myapp")?,
AccessFs::from_all(abi),
),
])?
.add_rules([
NetPort::new(443, AccessNet::ConnectTcp),
NetPort::new(8080, AccessNet::BindTcp),
])?
.restrict_self()?;
match status.ruleset {
RulesetStatus::FullyEnforced => eprintln!("landlock: fully enforced"),
RulesetStatus::PartiallyEnforced => eprintln!("landlock: partial (older kernel)"),
RulesetStatus::NotEnforced => return Err("landlock unsupported".into()),
}
Ok(())
}
fn main() -> io::Result<()> {
sandbox().expect("sandbox setup");
// 正常业务代码...
Ok(())
}
跟 Go 版本类似,Rust crate 用 ABI::V4 声明期望的 ABI,而 RulesetStatus 让调用者显式决定"部分兼容能否接受"。安全敏感的场景(比如加密守护进程或凭据代理)应该在 PartiallyEnforced 时直接失败退出,而不是降级运行。这也是我一直强调的原则:降级模式对可用性友好,但对安全审计不友好,别混用。
与 systemd 单元集成 Landlock
如果你不想修改二进制,systemd v253 起提供了原生 Landlock 单元指令,由 pid 1 在启动服务前完成规则集创建与 restrict_self。在 Fedora 40+ 与 Debian 13 上开箱可用。
# /etc/systemd/system/myapp.service
[Unit]
Description=Landlock-sandboxed HTTP service
[Service]
ExecStart=/usr/local/bin/myapp
User=myapp
DynamicUser=yes
# 传统 systemd 沙箱(仍然重要,与 Landlock 叠加)
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
SystemCallFilter=@system-service ~@privileged
# Landlock 指令(systemd 253+)
LandlockAccessFS=read-only:/etc read-only:/usr write:/var/lib/myapp
LandlockNetworkBind=tcp:8080
LandlockNetworkConnect=tcp:443
[Install]
WantedBy=multi-user.target
systemd 会自动做 ABI 协商:请求的能力若内核不支持,单元启动时会记录 warning 但不会失败(和 BestEffort 语义一致)。如果要严格模式,追加 LandlockRequireVersion=4 强制 ABI ≥ v4,不满足则拒绝启动。这套组合跟我们在 SELinux 强制访问控制实战里提到的三层加固思路互补:SELinux 提供发行版级别的 MAC 兜底,systemd 提供命名空间与 cgroup 隔离,Landlock 提供进程自陈述的最小权限。三者叠起来,单点绕过成本大幅上升。
ABI v4+ 网络范围限制:TCP 绑定与出站控制
ABI v4 首次给 Landlock 补上了网络控制能力,但目前只覆盖 TCP 层的 bind(2) 与 connect(2),粒度是端口号。UDP、UNIX 套接字(非抽象)、原始套接字都不在其管辖内,若要限制这些,仍需 seccomp 或 SELinux。抽象 UNIX 套接字要到 ABI v6 才通过 SCOPE 机制覆盖。
/* C 语言添加网络规则 */
struct landlock_ruleset_attr attr = {
.handled_access_fs = /* ... */,
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
};
int rs = landlock_create_ruleset(&attr, sizeof(attr), 0);
/* 允许出站到 443 端口(与任何目标 IP) */
struct landlock_net_port_attr port = {
.allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
.port = 443,
};
landlock_add_rule(rs, LANDLOCK_RULE_NET_PORT, &port, 0);
/* 允许绑定 8080 端口(监听) */
port.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP;
port.port = 8080;
landlock_add_rule(rs, LANDLOCK_RULE_NET_PORT, &port, 0);
这非常适合反向代理与抓取器类应用:web 后端只需 bind 8080、connect 5432(PostgreSQL)与 6379(Redis),把 SSRF 与横向移动的攻击面切到几乎为零。搭配 eBPF 安全监控做出站流量审计,可在应用尝试访问未列入白名单的端口时秒级告警。
生产模板:web 服务与 CI 构建器沙箱
下面两套模板已经在我维护的内部服务中运行超过 8 个月,可直接抄用。第一套针对 Go 编写的 HTTP 服务,第二套针对多语言的 CI 构建器(gitlab-runner exec / GitHub Actions self-hosted)。
web 服务模板(go-landlock + systemd)
// 在服务代码 main() 最早期
err := landlock.V6.BestEffort().RestrictPaths(
landlock.RODirs("/etc/ssl/certs", "/etc/resolv.conf", "/usr/share/zoneinfo"),
landlock.RODirs("/etc/myapp"), // 配置只读
landlock.RWDirs("/var/lib/myapp", "/var/log/myapp"), // 数据可写
landlock.RWDirs("/tmp"), // 临时文件
).RestrictNet(
landlock.BindTCP(8080),
landlock.ConnectTCP(5432), // Postgres
landlock.ConnectTCP(6379), // Redis
landlock.ConnectTCP(443), // 出站 API
).RestrictSignal().RestrictAbstractUnixSocket() // ABI v6
if err != nil { log.Fatal(err) }
CI 构建器模板(landlock-exec 包装)
# 在 job runner 中包装每个构建步骤
landlock-exec \
--ro /usr /etc /var/cache/apt \
--rw /workspace /tmp \
--net-connect 443,80 \
--net-bind 0 \
-- bash -c "cd /workspace && make build test"
landlock-exec 是社区维护的 CLI 包装器,类似 sudo 但反向:让子进程运行在更严格的沙箱中,而不是更宽松的特权中。CI 场景下配合每次运行的临时 /workspace 挂载,即便构建脚本被投毒也没法读取宿主机 SSH 密钥或写入 /etc/cron.d。这是 2024 年多次公开供应链事件的核心利用路径,值得每个 self-hosted runner 都套一层。
排查常见错误:EACCES、部分兼容与 no_new_privs
Landlock 出问题往往是静默的。应用只会看到 open() 返回 Permission denied,并不知道是哪一层拒绝的。老实说,我第一次写完就在这上面卡了半小时,以为是权限写错了,结果是漏了 no_new_privs。以下检查清单能覆盖 90% 的故障。
- 确认 no_new_privs:
grep NoNewPrivs /proc/<pid>/status 应为 1。为 0 说明 landlock_restrict_self 从未成功。
- 确认 Landlock 已附着:读
/proc/<pid>/status 中的 Landlock 字段应显示活动层数(需要 Linux 6.3+ 的 procfs 扩展)。
- 用 strace 定位越界:
strace -e openat,connect,bind -f ./myapp 逐条查找返回 EACCES/EPERM 的调用,再回到 Landlock 规则列表补齐白名单。
- ABI 协商失败:若
landlock_create_ruleset 返回 -EOPNOTSUPP,说明请求了当前内核不支持的能力。用 BestEffort 模式,或在应用启动日志中记录实际生效的 ABI 供审计。
- 路径符号链接:Landlock 检查解析后的真实路径。若把
/var/run/myapp.sock 加入白名单,而 /var/run 是指向 /run 的 symlink,规则实际生效在 /run/myapp.sock 上。用 readlink -f 提前解析。
- fork/exec 后失效? 不会。Landlock 层通过 fork 与 execve 继承,子进程无法解除父进程施加的限制。这是它比 chroot 更可靠的原因之一。
常见问题解答
Landlock 能替代 SELinux 或 AppArmor 吗?
不能替代,但可以互补。SELinux/AppArmor 由系统管理员维护统一策略,覆盖所有进程包括恶意二进制;Landlock 依赖应用自愿收缩,恶意程序可以选择不启用。生产环境应同时启用两者,让 MAC 兜底、Landlock 提供开发者已知的最小权限。
哪个内核版本开始生产可用?
Linux 6.2(ABI v3)是文件沙箱的最低生产可用版本,支持 TRUNCATE 等 90% 场景;若要网络控制,需要 Linux 6.7(ABI v4)或更新。RHEL 9.4、Ubuntu 24.04 LTS 与 Debian 13 均满足要求,可以放心在生产集群里推广。
Landlock 会带来性能损失吗?
每次文件路径 permission 检查是 O(层数 × 路径分量),典型场景开销小于 1 微秒,对绝大多数应用不可测量。网络规则是端口号哈希查找,基本零开销。Kernel 6.11 起 Landlock 的路径匹配算法进一步优化,深嵌套目录也在纳秒级。
Landlock 与 chroot、命名空间比有什么优势?
chroot 只限制路径可见性,不限制打开权限,并可被 root 或有 CAP_SYS_CHROOT 的进程绕过。命名空间需要 CAP_SYS_ADMIN 或 unshare 权限。Landlock 不需要任何 capability,普通用户即可自愿沙箱化,并且规则会通过 fork/exec 继承给子进程。
如何在容器里使用 Landlock?
容器运行时(Docker/Podman/containerd)默认不阻止应用调用 Landlock 系统调用,只要宿主机内核支持即可在容器内启用。注意默认 seccomp 白名单在旧版 Docker 中曾漏掉 landlock_* 三个 syscall,升级到 Docker 25+ 或 containerd 1.7+ 已修复。