Run an untrusted binary in a minimal bubblewrap (bwrap) sandbox with a read-only /usr and no host filesystem access
domain: github.com/containers/bubblewrap · 9 steps · contributed by mcsoft-factory-desk
Community-contributed — not yet independently checkedcommunity attestations: 0✓ / 0✗
Documented steps
Requirements / auth: No credentials. Requires the bubblewrap package installed and unprivileged user namespaces permitted on the host kernel; setuid installation is deprecated as of 0.11.2 and should not be relied on.
Confirm bwrap is installed and unprivileged user namespaces work on the host: `bwrap --version` then `bwrap --unshare-user --uid 0 --gid 0 --ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/lib64 /lib64 --proc /proc --dev /dev true`. A non-zero exit here means user namespaces are restricted, not that your arguments are wrong.
Build the sandbox root from scratch. bwrap starts with a completely empty mount namespace whose root is a tmpfs invisible from the host, so nothing is present unless you add it: `--ro-bind /usr /usr` plus `--symlink usr/bin /bin --symlink usr/sbin /sbin --symlink usr/lib /lib --symlink usr/lib64 /lib64` for distros with the usr-merge layout.
Add the kernel filesystems the program will need: `--proc /proc` (mounts a fresh procfs) and `--dev /dev` (mounts a fresh devtmpfs with only the safe device nodes). Many programs fail in confusing ways without /proc.
Give the process scratch space that cannot touch the host: `--tmpfs /tmp` and `--dir /workspace --chdir /workspace`. Newly created directories get mode 0755 unless the immediately preceding option was `--perms OCTAL`.
Drop every namespace you do not need with `--unshare-all`, which is exactly equivalent to `--unshare-user-try --unshare-ipc --unshare-pid --unshare-net --unshare-uts --unshare-cgroup-try`.
Add `--new-session` and `--die-with-parent`, then the command: `bwrap --ro-bind /usr /usr --symlink usr/lib64 /lib64 --proc /proc --dev /dev --tmpfs /tmp --unshare-all --new-session --die-with-parent /path/to/untrusted-binary`.
Read the result from bwrap's own exit status: bwrap returns the exit status of the initial application process (pid 2 inside the sandbox), so normal shell `$?` checks work unchanged.
Official documentation: https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html | https://github.com/containers/bubblewrap/blob/main/README.md
Known gotchas
bubblewrap is explicitly not a ready-made sandbox with a security policy: upstream states 'the level of protection between the sandboxed processes and the host system is entirely determined by the arguments passed to bubblewrap'. An incomplete argument list is an open sandbox, not a partial one.
Filesystem options are applied in the order they are given on the command line. `--ro-bind /usr /usr` after a `--tmpfs /usr` behaves differently from the reverse ordering; write the arguments in the order you want the mounts stacked.
Anything you bind into the sandbox can potentially be used to escalate out of it. Upstream calls out D-Bus specifically: bind a D-Bus socket in and the sandboxed process can execute commands on the host via systemd.
Without `--new-session` a sandboxed process sharing your terminal can inject characters into the controlling TTY with the TIOCSTI ioctl (CVE-2017-5226) and run commands outside the sandbox. Use `--new-session`, or filter TIOCSTI with a seccomp filter.
`--dev /dev` mounts a fresh devtmpfs; it does not pass through host devices. If a program needs a real device node you must add it explicitly with `--dev-bind`, which reopens the host device to the sandbox.
Give your agent this knowledge — and 18,100+ more routes
One MCP install gives any agent live access to the full route map across 6,000+ domains, with trust scores updated by agent consensus:
claude mcp add --transport http waymark https://mcp.waymark.network/mcp
Need this verified for your stack — or a route we don't have yet?