Blog
5 min read

Landlock: Unprivileged Sandboxing Built Into the Linux Kernel

Landlock lets an unprivileged process restrict its own filesystem and network access, permanently, for itself and its children. How rulesets, access rights and ABI versions work, a minimal C/Rust-style walkthrough, how it compares with seccomp, namespaces and AppArmor, and why coding agents use it.

Most Linux isolation mechanisms need privileges to set up: namespaces need user namespaces (often restricted), AppArmor and SELinux profiles are written by administrators, seccomp filters syscalls but knows nothing about paths. Landlock fills a specific gap: an unprivileged process can restrict its own access to files (and, in newer kernels, network ports) — and the restriction is irrevocable and inherited by every child.

It's a Linux Security Module (LSM), merged in Linux 5.13 (2021), stackable with whatever other LSMs the system uses.

The model

  1. Create a ruleset declaring which access rights you want to handle (i.e. restrict).
  2. Add rules granting specific rights beneath specific paths (or to specific ports).
  3. Restrict yourself. From then on, any handled access not granted by a rule is denied — for this thread and everything it spawns.

Three syscalls do this: landlock_create_ruleset, landlock_add_rule, landlock_restrict_self.

Rulesets stack: a process can add more Landlock domains later, but each new one can only further restrict. There's no way to lift a restriction.

Access rights

Filesystem rights include, among others:

  • EXECUTE, READ_FILE, WRITE_FILE, READ_DIR
  • MAKE_REG, MAKE_DIR, MAKE_SYM, REMOVE_FILE, REMOVE_DIR
  • REFER (renaming/linking across directories), TRUNCATE, IOCTL_DEV

Network rights: BIND_TCP and CONNECT_TCP to specific ports.

Scoping: restrictions on connecting to abstract UNIX sockets and sending signals outside the domain.

Rules are path-beneath: granting READ_FILE on /usr allows reading anything under /usr. They're attached to the opened file hierarchy, not path strings, so symlink and rename tricks don't escape them.

ABI versions: feature detection matters

Landlock has grown in versions, and you must ask the kernel what it supports:

ABI Kernel Added
1 5.13 Core filesystem rights
2 5.19 REFER (cross-directory rename/link)
3 6.2 TRUNCATE
4 6.7 TCP bind/connect
5 6.10 IOCTL_DEV
6 6.12 Scoping of abstract UNIX sockets and signals
int abi = syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);

The recommended pattern is best-effort: declare the rights your code knows about, mask out those the running kernel doesn't support, and degrade gracefully. Libraries (the landlock Rust crate, Go's go-landlock, Python bindings) handle this compatibility logic for you.

A walkthrough

Sandbox a process so it can read system files and its project directory, write only to the project, and connect only to port 443:

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_REMOVE_FILE | LANDLOCK_ACCESS_FS_EXECUTE,
    .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP | LANDLOCK_ACCESS_NET_BIND_TCP,
};
int rs = syscall(SYS_landlock_create_ruleset, &attr, sizeof(attr), 0);

/* read + execute under /usr */
struct landlock_path_beneath_attr usr = {
    .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR |
                      LANDLOCK_ACCESS_FS_EXECUTE,
    .parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
syscall(SYS_landlock_add_rule, rs, LANDLOCK_RULE_PATH_BENEATH, &usr, 0);

/* read + write under the project */
struct landlock_path_beneath_attr proj = {
    .allowed_access = attr.handled_access_fs & ~LANDLOCK_ACCESS_FS_EXECUTE,
    .parent_fd = open("/home/dev/project", O_PATH | O_CLOEXEC),
};
syscall(SYS_landlock_add_rule, rs, LANDLOCK_RULE_PATH_BENEATH, &proj, 0);

/* outbound HTTPS only */
struct landlock_net_port_attr https = { .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP, .port = 443 };
syscall(SYS_landlock_add_rule, rs, LANDLOCK_RULE_NET_PORT, &https, 0);

prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);   /* required for unprivileged restriction */
syscall(SYS_landlock_restrict_self, rs, 0);
close(rs);
execvp(argv[0], argv);

(Error handling and ABI masking omitted.) PR_SET_NO_NEW_PRIVS is mandatory without CAP_SYS_ADMIN: it guarantees the sandboxed process can't regain privileges through setuid binaries.

What Landlock doesn't do

Landlock vs the alternatives

Landlock seccomp Namespaces (bubblewrap) AppArmor/SELinux
Unprivileged Yes Yes Needs user namespaces No (admin policy)
Restricts FS paths, TCP ports Syscalls What's visible Paths, caps, network (MAC)
Self-applied by app Yes Yes Via a launcher No
Stackable Yes Yes — Typically one major LSM

Why coding agents use it

AI coding agents run commands generated by a model that may have read hostile text. Landlock is attractive because an agent CLI can sandbox the commands it spawns without root and without a daemon: write access to the workspace only, reads of system paths, optional network restriction. OpenAI's Codex CLI, for instance, uses Landlock with seccomp on Linux; other tools use bubblewrap (namespaces) for the same goal. (Bubblewrap, Claude Code sandbox, Prompt injection)

Check availability with cat /sys/kernel/security/lsm (look for landlock); some distributions need it enabled at boot.


EasySpawn puts a VM boundary around each server, so in-process sandboxes like Landlock become defence in depth rather than the only thing between an agent and your other projects. See how it works or join the waitlist.

Related: Bubblewrap · Hardening Containers With seccomp, AppArmor and User Namespaces · Linux Capabilities · Run AI-Generated Code Safely

Keep reading