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
- Create a ruleset declaring which access rights you want to handle (i.e. restrict).
- Add rules granting specific rights beneath specific paths (or to specific ports).
- 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_DIRMAKE_REG,MAKE_DIR,MAKE_SYM,REMOVE_FILE,REMOVE_DIRREFER(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
- Not a syscall filter — it governs access rights on objects, not arbitrary syscalls. Pair it with seccomp to shrink kernel attack surface. (Container hardening with seccomp)
- Network coverage is narrow — TCP bind/connect by port. No UDP, no host/IP filtering. Use a network namespace plus an egress proxy for domain allow-lists. (Egress control for AI agents, Linux namespaces)
- Not a resource limit — CPU and memory need cgroups. (Container CPU and memory limits)
- Kernel bugs remain shared — it's still the host kernel. Stronger boundaries need VMs. (Firecracker vs gVisor vs containers)
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
WebAssembly as a Sandbox: Running Untrusted Code With Wasmtime and WASI
WebAssembly's design makes it a strong in-process sandbox: linear memory, no ambient authority, and capability-based access through WASI. How the isolation works, limiting CPU with fuel and epochs, memory limits, the component model, real uses for plugins and user code, and where it falls short.
Linux Capabilities: Splitting Root Into Pieces
Linux capabilities break root's power into about 40 separate privileges, so a process can have just the one it needs. The capability sets (permitted, effective, inheritable, bounding, ambient), file capabilities with setcap, Docker's default set and --cap-drop ALL, why CAP_SYS_ADMIN is 'the new root', and inspecting capabilities.