Linux Landlock LSM 无特权沙箱实战 2026:ABI v6 网络范围限制、systemd 集成与应用加固完整指南

从 ABI v1 到 v6 一步步吃透 Landlock:C/Go/Rust 三种语言的最小可运行代码、systemd 单元集成、TCP 端口白名单、生产模板与常见 EACCES 故障排查清单。

Linux Landlock LSM 沙箱指南 (2026)

最后更新:2026 年 8 月 13 日

Linux Landlock 是内核 5.13 起内置的无特权 LSM 沙箱框架,允许任何普通进程无需 root 权限,仅用几十行代码就能给自己套上文件与网络访问白名单,任何越界的 open、rename 或 TCP 连接都会返回 EACCES/EPERM。到 2026 年,主流发行版内核(Linux 6.12+)的 Landlock ABI 已推进到 v6,新增了网络范围限制、UNIX 抽象套接字与信号作用域控制,足以覆盖大多数用户态服务的最小权限需求,成为继 seccomp 之后应用侧防御的第二支柱。

  • 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_rulesetlandlock_add_rulelandlock_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 就万事大吉"的误区。下表按开发者与运维最关心的六个维度对比。

维度Landlockseccomp-BPFAppArmorSELinux
约束粒度资源(路径/端口)系统调用号 + 参数路径 + 网络类型 + 域
特权要求无(应用自愿)无(应用自愿)root 加载策略root 加载策略
策略持有者开发者/打包者开发者系统管理员发行版 + 管理员
可堆叠是(嵌套层)是(过滤链)
网络控制ABI v4+:TCP 端口syscall 级别规则较粗细粒度
典型内核5.13+3.5+2.6.36+2.6+

实践里的组合拳:seccomp 拦截 execveptrace 之类高风险系统调用;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内核版本发布时间新增能力
15.132021-06基础文件权限:EXECUTE/READ/WRITE/READDIR + 目录创建/删除
25.192022-07LANDLOCK_ACCESS_FS_REFER(跨挂载/目录 rename 与 link)
36.22023-02LANDLOCK_ACCESS_FS_TRUNCATE(独立 truncate/ftruncate 控制)
46.72024-01网络:LANDLOCK_ACCESS_NET_BIND_TCP、CONNECT_TCP
56.102024-07LANDLOCK_ACCESS_FS_IOCTL_DEV(设备节点 ioctl 白名单)
66.122024-11LANDLOCK_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% 的故障。

  1. 确认 no_new_privs:grep NoNewPrivs /proc/<pid>/status 应为 1。为 0 说明 landlock_restrict_self 从未成功。
  2. 确认 Landlock 已附着:读 /proc/<pid>/status 中的 Landlock 字段应显示活动层数(需要 Linux 6.3+ 的 procfs 扩展)。
  3. 用 strace 定位越界:strace -e openat,connect,bind -f ./myapp 逐条查找返回 EACCES/EPERM 的调用,再回到 Landlock 规则列表补齐白名单。
  4. ABI 协商失败:若 landlock_create_ruleset 返回 -EOPNOTSUPP,说明请求了当前内核不支持的能力。用 BestEffort 模式,或在应用启动日志中记录实际生效的 ABI 供审计。
  5. 路径符号链接:Landlock 检查解析后的真实路径。若把 /var/run/myapp.sock 加入白名单,而 /var/run 是指向 /run 的 symlink,规则实际生效在 /run/myapp.sock 上。用 readlink -f 提前解析。
  6. 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+ 已修复。

文章更新记录 (1)
  • — SEO meta refreshed (title and description updated)
Yuki Tanaka
关于作者 Yuki Tanaka

Linux kernel security engineer with a background in eBPF and LSM. Likes hardening more than she likes sleeping.