Kernel-enforced sandboxing for untrusted processes. Two zero-dependency core tools, one shared profile format, plus an optional BPF-LSM module.
v1.4.1 note: documentation and test correction only. An
execallow-list that lists the dynamic loader is a hardening layer, not an exec-target boundary:ld.so <a compatible loadable ELF>runs that file with no execute check on it. Two shapes close the direct-loader bypass — static-link the allowed binaries and do not list the loader, or grant no location that is both readable and writable. Anoexecmount over the writable areas closes only the writable-payload half of it and is not a third complete answer. Even with the bypass closed the result is an exec-target boundary and not a code-execution boundary: an allowed program that is compromised can still interpret, JIT,dlopenor map code of its own. Separately,roon a directory grants execute on every file beneath it, soexamples/restricted-root.confnow grants its library and read-only-data directoriesrwand pairs each one withmount-ro:rootdir-flags rois applied non-recursively —/proc,/devand/syshave to stay usable — so it does not on its own make the write right of arwrule inert. The HOWTO, SECURITY.md, the man pages and the example profiles say all of this, and the Landlock suites witness it on every run.
v1.4.0 note:
compartment-rootnow starts and runs correctly under real root, and is tested there by root-only suites (sudo make test-root); several of its container fixes are security fixes, so read the upgrade notes before deploying it. Profile trust changed: a profile is only honoured if its source is trusted, the parser is transactional (a file that fails to parse changes nothing), the security switches are one-way — a profile may tighten policy but never loosen it — and$HOMEis no longer searched unless--user-profilesis given. Landlock policy gained per-file rules and TCP port rules (net-bind,net-connect,net-default) on kernels with Landlock ABI v4 or newer.make installno longer deploys the example profiles;make install-profilesdoes. The optionalcompartment-bpfmodule moves to v0.8.0, port ABI 0x0008, which adds mount, ACL and ioctl coverage — and reclassifies timestamp writes asno-chmod-class, so on a directlyno-chmod-sealed file a non-actor is now deniedtouch,utimensat(2)and the timestamp-restoring tail ofcp -p,rsync -a,tar -x,install -pandunzip. Seecompartment-bpf/CHANGELOG.mdbefore upgrading a host that hasno-chmodseals in the field.
Note: This is an open-source Linux isolation toolkit, not a formally validated security product. The code has been through multiple review rounds and an automated test suite (run
make test-integrationandsudo make test-rootfor the current counts), but it has not undergone professional penetration testing or formal verification. The automated tests do not yet cover all bypass vectors (e.g. direct network egress in sandbox mode). compartment-root is covered by root-only suites — see tests/scripts/root.d/. Use it as a defense-in-depth layer, not as your sole security boundary. See DESIGN.md for documented limits and the full security review log.
| Tool | Purpose | Root? | Deps |
|---|---|---|---|
| compartment-user | Landlock + seccomp + env sanitize + audit | no | none |
| compartment-root | Full namespace container + seccomp + audit | yes | none |
| sandbox.sh | Network namespace + proxy bridge | no | unshare, ip; socat only with an upstream proxy; slirp4netns + nsenter only for the SOFT fallback |
| compartment-bpf | Optional BPF LSM inode sealing (kernel-side deny, even root) | yes (CAP_BPF + CAP_SYS_ADMIN) | clang ≥ 12, libbpf, bpftool, libsodium, BTF, kernel ≥ 6.6 with lsm=...,bpf |
compartment-user / compartment-root use Landlock + seccomp
(userspace policy) and are the default zero-dependency path.
compartment-bpf seals individual inodes at the BPF LSM level —
enforcement lives in the kernel and survives even root. The two
approaches are complementary. See compartment-bpf/HOWTO.md for
the BPF tool and its requirements.
make
./compartment-user -- /bin/sh # sandboxed shell in 2 commands
./compartment-user --dry-run -- /bin/sh # see what would be appliedmake # builds the zero-dependency core tools
make test # the rootless suites, quick mode (run_all.sh --quick)
make test-integration # every unprivileged suite (sandbox.sh, external CLI)
sudo make test-root # the root-only suites
make hardened # build with randomized shell stash path
make show-hardening # print the hardening flags this toolchain accepted
make check # shellcheck + file-mode checks (same gates as CI)Hardening flags (-fPIE -pie, full RELRO, -z noexecstack,
-fstack-clash-protection, -fcf-protection, _FORTIFY_SOURCE=3) are
stated by the Makefile rather than inherited from the distribution's gcc
specs, and each one a toolchain might not have is chosen by a compile-and-
link probe, so an older or non-x86 target drops it instead of failing.
Optional BPF module:
cd compartment-bpf
make vmlinux.h && makeRequires Linux kernel 6.6+ with BPF LSM enabled, plus clang, libbpf-dev, bpftool, libsodium-dev, and BTF for the running kernel.
# Sandbox an AI agent (Landlock + seccomp, rootless)
./compartment-user -- claude --model claude-opus-4-6
# Use a profile file
./compartment-user --profile strict -- codex --full-auto
# See what would be applied without running
./compartment-user --dry-run -- claude
# Full namespace container (requires root)
./compartment-root --profile examples/container.conf -- /bin/sh
./compartment-root --rootdir /srv/jail -U svc --audit -- /usr/bin/myapp
# Full isolation (network namespace + proxy + Landlock + seccomp)
./sandbox.sh claude --model claude-opus-4-6Compartment can lock down any network client — not just AI agents. Here is a worked example using SSH, showing how to split a process into privilege-separated components so that no single compromise can both access secrets AND exfiltrate them.
If a remote SSH server is compromised, it can reverse-exploit the SSH client. A trojanized client could:
- Write stolen credentials to
~/.ssh/exfil.txt - Log keystrokes to a hidden file
- Exfiltrate data over the network to a third-party host
Lock the SSH client to read-only filesystem access. It can read keys to authenticate but cannot write anything to disk:
# One-liner: SSH with no filesystem writes
./compartment-user --profile examples/ssh.conf -- ssh user@host
# What happens if the SSH binary tries to write:
# touch /tmp/exfil.txt → EACCES (blocked by Landlock)
# echo x > ~/.ssh/log.txt → EACCES (blocked by Landlock)
# cat ~/.ssh/id_ed25519 → OK (read allowed)Split SSH into two sandboxed processes with complementary restrictions:
┌──────────────────────────┐ ┌──────────────────────────┐
│ SSH (read-only fs) │────▶│ socat (no user files) │────▶ remote:PORT
│ • can read keys │ │ • no $HOME access │
│ • cannot write anywhere │ │ • cannot read SSH keys │
│ • Landlock + seccomp │ │ • Landlock + seccomp │
└──────────────────────────┘ └──────────────────────────┘
localhost:RANDOM_PORT
# Paranoid SSH to a remote server
./examples/paranoid-ssh.sh user@remote-host
# With a custom port
./examples/paranoid-ssh.sh user@remote-host -p 2222
# Run a command
./examples/paranoid-ssh.sh user@remote-host "uptime"Security properties:
- SSH process can read
~/.ssh/keys but cannot write to disk → a reverse-exploited SSH cannot save stolen data locally - socat process has network access but cannot read any user files → even if socat is exploited, attacker cannot access credentials
- Neither process alone can both access secrets AND exfiltrate them
This is the same principle as OpenSSH's own privilege separation, but applied at the OS level with Landlock + seccomp instead of trusting the application to separate itself.
Traditional sysadmin thinking: "SSH is trusted, the network is untrusted."
Compartment thinking: "Nothing is fully trusted. Split every process so that compromise of any single component cannot achieve both data access and data exfiltration."
This pattern applies to any network client:
- curl/wget — read-only profile prevents saving downloaded malware
- git — read-only profile for fetch, write-only for the workdir
- database clients — prevent credential logging to disk
- AI agents — the primary use case (see
sandbox.sh)
compartment-user applies kernel-enforced restrictions before exec:
Kernel-enforced, inherited across fork/exec, and impossible for the
sandboxed process or its descendants to remove:
PR_SET_NO_NEW_PRIVS— prevent privilege escalation- Landlock — filesystem path restrictions (read-only system paths,
writable workdir) and, from ABI v4 (Linux 6.7), TCP port restrictions.
A rule may name a directory or a single file:
rw /dev/nullandexec /usr/bin/psqlare per-file grants, and a policy that grants execute on individual files and on no directory is a binary allow-list - seccomp BPF — block dangerous syscalls (ptrace, mount, kexec, bpf, io_uring, ...)
Applied once, immediately before exec, and not kernel restrictions:
- Environment sanitize — strip
LD_*, cloud credentials, SSH agent socket, etc. from the environment handed to the command. A sandboxed process can re-export any of them for its own children and the loader will honour it; usecompartment-bpffor durable environment policy. - Working directory and file-descriptor cleanup — one-time actions.
- Audit logging — a record, not a restriction: file-per-day log with PPID chain.
Run-time order: audit log → preflight → no_new_privs → environment →
Landlock → seccomp → chdir → hardening → exec.
Two properties of Landlock decide how a policy has to be written:
- It is additive. The rights of every rule matching an ancestor of the
path being opened are unioned, so a narrower rule never takes anything
away from a wider one.
rw /workplusro /work/secretsleaves the secrets writable. Both tools refuse that policy rather than pretend. - A rule for a path that does not exist grants nothing. That is now a
fatal error; append
?to the path (ro /lib32?) for the entries that are genuinely conditional.--verbosereports how many rules were installed, not how many were asked for.
compartment-root creates a fully isolated container:
clone()with new UTS, mount, PID, IPC, net, user, cgroup namespaces- pivot_root — the new root is bind-mounted
nosuid,nodevonto itself first, so a setuid binary inside it cannot elevate (stronger than chroot).rootdir-flagsaddsnoexecandroon top;mount-ro,mount-noexec,mount-nosuidandmount-nodevdo the same for one path at a time. The rootdir itself must be root-owned (or owned by the mapped host uid of the container's root under a shifteduid-map) and not group- or world-writable - Fresh
/proc, read-only/sys, a/devtmpfs withnull,zero,full,random,urandomandttybind-mounted from the old root, a privatedevptson/dev/ptswith/dev/ptmxbound to it, a tmpfs/dev/shm, 15 masked/procpaths, isolated hostname — all of it applied before the old root is detached, which is what the kernel'smount_too_revealing()check requires - Landlock, opt-in with
landlock on— the same ruleset builder compartment-user uses, applied after the mounts and before the privilege drop, soexec /usr/bin/psqlis a per-binary allow-list entry inside the container - Capability drop — raw prctl + capset, no libcap.
cap-allowpreserves named capabilities for the service user viaPR_SET_KEEPCAPS+capset() - seccomp BPF — raw BPF, no libseccomp; a 43-syscall deny-list is
installed by default, and
seccomp-defaultchooses what a denied call does (errno,killorlog) - Environment sanitize + audit logging (same as compartment-user),
FD cleanup,
PR_SET_NO_NEW_PRIVS(which no profile can turn off), and a minimal PID 1 reaper that forwards signals and reaps orphans — itself under the same filter whenever the policy permits the syscalls it needs
sandbox.sh wraps the command in a network-isolated user+mount namespace:
unshare --user --mount --net --map-root-user --fork— HARD mode: loopback-only, no external interfaces, no routes. Needs an unprivileged user namespace with a uid map, which some distributions block; run./sandbox.sh --verifyto see whether this host allows it. SOFT fallback: slirp4netns with--disable-host-loopback, which also needsnsenter- Unix socket proxy bridge — API traffic routed through corporate proxy
- Bind-mount shell replacement — every
/bin/bashsubprocess gets sandboxed (requires mount namespace, which sandbox.sh creates)
In HARD mode with a proxy configured, the namespace has no external interfaces — network traffic is intended to flow only through the unix socket proxy bridge. (The automated tests verify proxy reachability but do not yet include direct-bypass resistance tests.)
Both tools read the same .conf syntax. The filesystem, network, syscall
and environment directives are honoured by both; the namespace and mount
directives are compartment-root only and are parsed and silently ignored by
compartment-user, so keep the two kinds in separate files.
| Directive | Tools | Meaning |
|---|---|---|
ro PATH |
both | Read + execute on PATH and everything beneath it |
rw PATH |
both | Read + write, no execute (W^X) |
rwx PATH |
both | Read + write + execute |
exec PATH |
both | Read + execute. On a file this is a per-binary grant — see below |
PATH? |
both | A trailing ? makes the rule optional: skipped when the path is absent instead of fatal |
workdir PATH |
compartment-user | Working directory, added as rwx (not the W^X rw) |
net-bind PORT |
both | Allow bind(2) on this TCP port (repeatable) |
net-connect PORT |
both | Allow connect(2) to this TCP port (repeatable) |
net-default deny|ignore |
both | deny handles TCP bind and connect and refuses every port not listed. Default ignore: the network is not restricted |
block NAME |
both | Deny-list one syscall |
allow NAME |
both | Switch to allow-list mode and permit one syscall |
seccomp-mode allow |
both | Switch to allow-list mode explicitly. Accepts allow/allowlist and deny/denylist; any other value is a fatal parse error |
seccomp-default errno|kill|log |
both | What a denied syscall does. Default errno (EPERM) |
env-deny NAME / env-allow NAME |
both | Environment policy; a trailing * is a prefix match |
env-mode allow |
both | Switch the environment policy to allow-list mode. Accepts allow/allowlist and deny/denylist; any other value is a fatal parse error |
landlock/seccomp/no-new-privs/env-sanitize on |
both | One-way switches: a profile may turn a mechanism on, never off. Landlock defaults to on for compartment-user and off for compartment-root |
audit on / audit-log DIR |
both | Audit trail |
inherit NAME |
both | Load another profile first, then apply these rules on top |
rootdir DIR |
compartment-root | The container root. Must be root-owned and not group- or world-writable |
rootdir-flags LIST |
compartment-root | Extra mount flags for the rootdir bind: ro, noexec (nosuid and nodev are always applied). Applied non-recursively — submounts, /proc, /dev and /sys included, keep their own flags |
mount-ro PATH |
compartment-root | Bind PATH onto itself inside the new root and remount it read-only, recursively where the kernel has mount_setattr(2). This is what makes the write right of a rw library rule inert |
mount-noexec / mount-nosuid / mount-nodev PATH |
compartment-root | The same, for the other three flags (repeatable) |
uid / gid / username |
compartment-root | The service user to drop to |
uid-map / gid-map |
compartment-root | <container-start> <host-start> <count>; default is the identity map |
netns NAME |
compartment-root | Join /var/run/netns/NAME instead of creating a new network namespace |
cgroup PATH / cap-allow CAP / loopback on / mount-mask PATH |
compartment-root | Cgroup, capability, loopback and masking policy |
exec on a file is a binary allow-list. A Landlock rule may name a
regular file, and a rule on a file carries only the file-level rights. So
landlock on
exec /usr/bin/exampled
exec /usr/bin/psql
rw /usr/lib # libraries need read; `rw` carries no execute rightmeans "these two files may be the target of an execve(2) and nothing
else", because no directory in the policy carries execute. Three things to
know before relying on it. The dynamic loader has to be listed as well
(execve(2) opens the ELF interpreter with FMODE_EXEC, so Landlock checks
execute on it), while shared libraries do not (ld.so opens them
read-only, and Landlock has no mmap hook — read on the library directory is
enough). The rule keys on the file, so a busybox-style multi-call binary
cannot be split into applets. And ro on a directory grants execute on
every file beneath it, which is why the library directory above is rw and
why no directory holding binaries may appear as ro in an allow-list
policy — ro /usr/lib would make /usr/lib/klibc/bin/true an execve
target that no exec rule names.
Once the loader is listed the allow-list is a hardening layer rather than an
exec-target boundary, because the loader will start a compatible loadable
ELF for anyone who can read it. SECURITY.md and HOWTO.md fact 5 give the
full statement and the two shapes that close that bypass; neither shape
turns the allow-list into a code-execution boundary.
Landlock network rules are TCP-only and allow-list-only. net-bind and
net-connect map onto LANDLOCK_RULE_NET_PORT, which covers bind(2) and
connect(2) on TCP and nothing else: no UDP, no unix sockets, no netlink,
no raw sockets, no listen/accept granularity, and no per-address rules —
a rule for port 443 allows connecting to port 443 on any address, v4 and v6
alike. There is no way to express "everything except port N"; that needs a
BPF LSM and is left to a sibling tool. Below Landlock ABI v4 (Linux 6.7)
both tools warn loudly that the port policy is not active.
# compartment-user: filesystem (Landlock)
ro /usr
rwx $HOME
# Syscalls
block ptrace
block mount
# Or allow-list mode:
# seccomp-mode allow
# allow read
# allow write
# Environment ('*' at the end is a prefix match)
env-deny LD_*
# Or allow-list mode:
# env-mode allow
# env-allow PATH
# Features — a profile may only turn these on; use --no-landlock,
# --no-seccomp or --no-env-sanitize on the command line to disable them
landlock on
seccomp on
no-new-privs on
env-sanitize on
audit on# compartment-root: namespace container
rootdir /srv/containers/default
uid 1000
gid 1000
username svc
loopback on
cap-allow net_bind_service
mount-mask /proc/keys
# Landlock inside the container (opt-in), mount hardening, TCP ports
landlock on
exec /usr/bin/myapp
rw /usr/lib
rw /srv/data
rootdir-flags ro
mount-ro /usr/lib
mount-noexec /tmp
net-connect 5432
net-default denyexamples/restricted-root.conf is a worked example of that shape, with a
comment block on what it does and does not guarantee.
Print the effective policy at any time with
compartment-user --dump-profile <name>.
Search order: --profile /path/file.conf → /etc/compartment/<name>.conf →
~/.config/compartment/<name>.conf (compartment-user with --user-profiles
only) → built-in. compartment-root searches /etc/compartment/ only.
Every profile file must be a regular file owned by root or by you (root only for compartment-root), in a directory with the same ownership, and neither may be group- or world-writable.
See HOWTO.md for full format reference.
Each profile addresses a different threat model. Pick the one that matches what you're protecting against.
| File | Use when | Protects against |
|---|---|---|
ai-agent.conf |
Running Claude, Codex, Gemini CLIs | Agent reads/writes outside working directory, spawns unexpected processes |
strict.conf |
Untrusted code, tighter than ai-agent | Same as above, smaller syscall surface |
ssh.conf |
Running SSH client on a box you don't fully trust | Compromised SSH binary writing credentials to disk |
socat-proxy.conf |
Used internally by paranoid-ssh.sh |
socat having access to your SSH keys |
container.conf |
Full namespace isolation via compartment-root | Process escaping its root directory |
dev.conf |
Development and debugging | Nothing — this is intentionally relaxed |
restricted-root.conf |
A service container with an exec binary allow-list and one outbound TCP port |
Anything but the named binaries running inside the container; egress to any other port |
curl-wget.conf |
Running curl, wget or aria2c |
An HTTP client reading or writing outside its download directory |
dns-client.conf |
Running dig, host, nslookup, drill, kdig |
A resolver tool touching anything but its own configuration |
net-trace.conf |
Running ping, traceroute, mtr, nmap, arping |
A raw-socket diagnostic tool with filesystem access it does not need |
tcpdump.conf |
Running tcpdump, tshark, dumpcap |
A capture tool reading the filesystem or writing outside its capture directory |
net-admin-ro.conf |
Read-only network administration: ip, ss, nft list, iptables -L |
A query command mutating network state or the filesystem |
tcp-udp-relay.conf |
Running socat, nc, ncat as a relay |
A relay tool reaching your files |
dhclient.conf |
Running the ISC DHCP client (root) | A DHCP client writing outside /var/lib/dhcp |
Which one should I use?
- Sandboxing an AI agent →
ai-agent.conf(default) orstrict.conf(tighter) - Connecting to a remote server →
paranoid-ssh.sh, which combinesssh.conf+socat-proxy.conf. The SSH process can read your keys but cannot write anywhere. The socat process handles the network connection but cannot read your keys. Neither alone can both steal credentials and exfiltrate them. - Running an untrusted service →
container.confwithcompartment-root - Figuring out why something is being blocked →
dev.conf, then tighten from there
Don't write profiles by hand. Use tools/syscall.py to generate one
for any program automatically:
# Step 1: Check — will the default profile break your program?
python3 tools/syscall.py check --profile ai-agent -- wget -q -O /dev/null https://example.com
# Step 2: If it breaks, generate a custom profile
python3 tools/syscall.py profile -o examples/wget.conf -- wget -q -O /dev/null https://example.com
# Step 3: Use it
./compartment-user --profile examples/wget.conf -- wget https://example.comWorks with anything: curl, git, ssh, rsync, python3, database
clients — any program you can run, you can profile and sandbox.
# More examples
python3 tools/syscall.py profile -o curl.conf -- curl -s https://example.com
python3 tools/syscall.py profile -o git.conf -- git clone https://github.com/user/repo
python3 tools/syscall.py profile -o psql.conf -- psql -c "SELECT 1"
# Strict allow-list (only permit observed syscalls, deny everything else)
python3 tools/syscall.py profile --seccomp-mode allow -o strict-curl.conf -- curl https://example.com
# See what syscalls a program actually uses
python3 tools/syscall.py trace -- ssh user@host "echo hello"Requires strace (apt install strace). See
tools/HOWTO-syscall-profiling.md
for the full guide.
compartment-user can transparently intercept /bin/bash so every
subprocess an AI agent spawns gets sandboxed:
/bin/bash (bind mount) → compartment-user
→ Landlock + seccomp applied
→ exec /bin/shells/bash (the real shell)
This happens automatically inside sandbox.sh when compartment-user
is built and available. See HOWTO.md for manual setup options.
Compartment can be deployed as the login shell for non-admin users,
so that every interactive session and every execve("/bin/sh", ...)
— including remote exploit payloads — enters a sandboxed shell
automatically.
This is an opinionated setup for controlled environments (hardened servers, jump boxes, CI runners), not a universal recommendation.
# Build with a randomized shell stash path
make hardened
# Output: REAL_SHELL_DIR=/bin/.shells_a1b2c3d4e5f6
# Preserve real shells in the stash directory
sudo mkdir -p /bin/.shells_a1b2c3d4e5f6
sudo mv /bin/bash /bin/.shells_a1b2c3d4e5f6/bash
sudo mv /bin/sh /bin/.shells_a1b2c3d4e5f6/sh
# Install compartment-user as the system shell
sudo cp compartment-user /bin/bash
sudo ln -sf /bin/bash /bin/sh
# Preserve a normal shell for the designated admin account
sudo chsh -s /bin/.shells_a1b2c3d4e5f6/bash root
sudo chsh -s /bin/.shells_a1b2c3d4e5f6/bash your-admin-userWhen invoked as bash or sh (detected via argv[0]),
compartment-user applies the ai-agent profile and execs the real
shell from the stash directory.
root / admin → /bin/.shells_.../bash (real shell, no sandbox)
all others → /bin/bash (compartment → sandboxed shell)
Landlock + seccomp + env sanitize + audit
A remote exploit that calls execve("/bin/sh", ...) gets compartment,
not bash. The payload hits Landlock filesystem restrictions and seccomp
syscall filtering before executing a single attacker-controlled
instruction. The sandboxed shell cannot ptrace, cannot load kernel
modules, cannot mount filesystems, and writes only to allowed paths.
- Compatibility: some workflows expect an unrestricted interactive shell and may break. Test thoroughly before deploying to production.
- Not a substitute for correct host hardening, patching, and privilege separation. This is a defense-in-depth layer.
- Bypass paths exist: an attacker who can write an ELF binary to
an executable path and invoke it directly (not through
/bin/sh) will bypass the shell-replacement layer. Landlock on the parent process limits where they can write, but this is not airtight. - Recovery: always keep at least one admin account with a real shell. If compartment-user has a bug, you need a way back in.
The same machinery, pointed at a uid-0 account: radmin logs in over
sshd, gets the wrapper as its login shell, and can inspect the system, read
logs and manage the paths a profile names — but cannot load a module, kexec,
write the raw disk, reach kernel memory, use ptrace/perf/bpf, leave its
namespace, edit anything in the authentication or login path, or ask systemd,
D-Bus or snapd to do any of it on its behalf.
Two directives make that possible on top of Landlock and seccomp:
| Directive | What it adds |
|---|---|
cap-drop CAP |
drops CAP from the bounding set, which for a root exec is the effective set the session inherits; one-way |
mask PATH |
covers PATH in a private mount namespace — the only way to take away connect(2) to a privileged unix socket, which is not a Landlock access right |
Ship examples/limited-root.conf and
compartment-bpf/profiles/limited-root-authpath.conf together: the first
binds the session, the second binds the inodes, and they answer different
adversaries. Full walk-through, the sshd settings it needs and the recovery
plan: HOWTO.md, "Limited root over SSH". Section 8 there is the
complete list of what this does not protect against — read it before
deploying, along with the self-protection section of
compartment-bpf/LIMITATIONS.md, which covers
the kernel-side half of the pair. tests/scripts/root.d/limited-root.sh
exercises the deployment end to end through a real sshd login, with an
unconfined uid-0 login as the positive control.
- Linux >= 5.13 (Landlock) — compartment-user
- Linux >= 4.6 (cgroup namespace) — compartment-root
- Linux >= 3.8 (user namespaces) — sandbox.sh
- No external libraries. No root for compartment-user.
compartment.h — shared code: profiles, audit, seccomp BPF, env sanitize
compartment-user.c — Landlock + seccomp + audit (zero deps, rootless)
compartment-root.c — Full namespace container (zero deps, requires root)
sandbox.sh — Network namespace + proxy bridge
Makefile — Build targets
README.md — This file
HOWTO.md — Detailed setup guide
DESIGN.md — Architecture, security review, lineage from shell-guard
SECURITY.md — Vulnerability reporting policy
LICENSE — Apache-2.0
.gitignore — Build products and operational artifacts, never committed
compartment-bpf/ — Optional BPF-LSM inode-sealing module, its own build,
profiles, tests and documentation (see its README.md,
HOWTO.md, LIMITATIONS.md and CHANGELOG.md)
examples/
ai-agent.conf — Profile for AI coding assistants
strict.conf — Locked-down profile (inherits ai-agent)
container.conf — Full namespace isolation profile (compartment-root)
restricted-root.conf — Container with an exec allow-list and one TCP port
dev.conf — Relaxed profile for development
ssh.conf — Read-only SSH client (no filesystem writes)
socat-proxy.conf — Network-only socat bridge (no user file access)
curl-wget.conf — HTTP(S) clients: curl, wget, aria2c
dns-client.conf — DNS clients: dig, host, nslookup, drill, kdig
net-trace.conf — Raw-socket diagnostics: ping, traceroute, mtr, nmap
tcpdump.conf — Packet capture: tcpdump, tshark, dumpcap
net-admin-ro.conf — Read-only network admin: ip, ss, nft list, iptables -L
tcp-udp-relay.conf — Relay tools: socat, nc, ncat
dhclient.conf — ISC DHCP client (root)
paranoid-ssh.sh — Privilege-separated SSH (SSH+socat split)
tools/
syscall.py — Profile generator: trace any program, emit .conf
HOWTO-syscall-profiling.md — Full guide to syscall profiling
man/
compartment-user.1 — Man page (section 1: user commands)
compartment-root.8 — Man page (section 8: system administration)
tests/
probes/deny_probe.c — Sandbox validation probe (machine-parseable output)
profiles/ — Test-specific .conf profile templates
scripts/run_all.sh — Rootless test runner (make test-integration)
scripts/run_root_tests.sh — Root-only test runner (sudo make test-root)
scripts/rootless.d/ — Discovered unprivileged suites (drop a script in)
scripts/root.d/ — Discovered root-only suites
README.md — Test documentation
scripts/
timestamp.sh — SHA256 + OpenTimestamps proof-of-existence
extra/
squid-proxy/, tinyproxy/ — Optional egress-proxy helpers
.github/workflows/
ci.yml — Build, rootless + root suites, sanitizers, lint
archive/
shell-guard/ — Archived shell-replacement tool (~2003, self-contained)
root required?
no yes
┌───────────────┬───────────────┐
filesystem │ compartment- │ compartment- │
restriction │ user │ root │
mechanism │ (Landlock) │ (pivot_root) │
│ │ │
│ Firejail │ bwrap (setuid)│
│ bwrap (userns)│ Docker/Podman │
├───────────────┼───────────────┤
no filesystem │ seccomp-only │ AppArmor │
restriction │ wrappers │ SELinux │
└───────────────┴───────────────┘
- Firejail (~100K lines) — closest comparison; mature profile ecosystem for desktop apps, but large attack surface with CVE history. compartment-user is well over an order of magnitude smaller and auditable in one sitting.
- bwrap (~3K lines) — mount/PID/network namespaces. Architecturally different (namespaces vs Landlock). Use bwrap when you need full mount isolation or kernel < 5.13; use compartment-user when you need profiles, shell-replacement, or work in containers where user namespaces are disabled.
- Minijail (Google) — expressive seccomp arg filtering, but requires libminijail. compartment-user trades arg filtering for zero-dep deployment.
- AppArmor/SELinux — system-wide MAC, finer granularity, but requires admin access and system policy installation. compartment-user is user-deployable with no system configuration changes.
No existing tool combines: zero deps, profile files with inheritance, shell-replacement mode, and PPID chain audit logging in under 4000 lines of C, header included.
- bubblewrap — Namespace-based sandboxing (complementary)
- firejail — Namespace + seccomp (setuid, profile files)
This project was developed with AI assistance:
- Claude Code (Anthropic) — primary coding, testing, debugging, and implementation across all C source, shell scripts, profiles, and test infrastructure
- ChatGPT (OpenAI), Gemini (Google), Codex (OpenAI) — independent code review rounds; DESIGN.md records all 61 findings and their fixes
- Human — architecture, design decisions, review coordination, and final approval
Apache-2.0. See LICENSE.