Running other people's programs is inherently insecure.
Rogue dependencies*
🎯 or hacked library code
🏴☠️ (et cet.
~/.ssh,~/.pki/nssdb/,~/.mozilla/firefox/<profile>/key4.db,~/.mozilla/firefox/<profile>/formhistory.sqlite...
✱ Running any Electron app relies on impeccability of hundreds or thousands of dependencies, NodeJS and Chromium to say the least! 😬
Run scary software in separate secure containers:
podman run --rm -it -v "$PWD:$PWD" --workdir="$PWD" \
--net=host debian:stable-slim ./scary-binaryor you can simply:
sandbox-run scary-binary(e.g. sandbox-run npx @google/gemini-cli)
which relies on unshare (from
util-linux package) to spawn your native OS "container" under the hood,
and (in case of npx @google/gemini-cli), after downloading almost 500 MB ❗ of JavaScript sources,
executes this untrusted third-party's Node/NPM package securely sandboxed,
with its CWD in $PWD and new root filesystem (/) in $PWD/.sandbox.
This script implements most of the functionality of
bubblewrap and
firejail
(bubblejail,
docker (podman), etc.—all
well-known Linux sandboxing tools that provide secure,
isolated environments for running untrusted programs)
in about ~400 lines of pure POSIX shell.
You're on a terminal. There's nothing to build. You run it. It works. The one major dependency chain is package util-linux, but if you can't trust Linux, can you even trust yourself?
Note
The repo also contains Bubblewrap wrapper
script sandbox-run.bwrap. If you trust Bubblewrap,
the wrapper script is shorter, slightly faster, and maybe easier to look-over/review,
but feature parity is limited and could not be strictly maintained.
Contributions welcome!
On Linux, there are no dependencies other than a POSIX shell with its standard set of conventions and utilities. The installation process might be similar on Windos/WSL. For macOS, see section Alternatives below.
# Install the unlikely-to-be-missing dependencies
sudo apt install mount coreutils util-linux
# Optionally install slirp4netns for separate network namespace.
# If unavailable, the network is shared with the host.
sudo apt install slirp4netns
# Optionally install `enosys` tool for automatic seccomp filtering
sudo apt install util-linux-extra
# Linux user namespaces need to be enabled
sudo sysctl -w kernel.unprivileged_userns_clone=1
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
# Download the script and put it somewhere on PATH.
# If you prefer the Bubblewrap wrapper, use
# URL 'https://bit.ly/sandbox-run-bwrap' instead.
curl -vL 'https://bit.ly/sandbox-run' | sudo tee /usr/local/bin/sandbox-run
sudo chmod +x /usr/local/bin/sandbox-run # Mark executable
sandbox-run
# Usage: sandbox-run ARG...
sandbox-run ls /Whenever you want to run an untrusted executable, simply run:
sandbox-run scary-app argsto run scary-app in a secure native sandbox.
"$PWD/.sandbox" contains the sandbox root filesystem (/).
When different from $HOME, the
current working directory ($PWD) is mounted with read-write permissions
while everything else required for a successful run (e.g. /usr, /lib)
is mounted read-only.
Directories matching $PWD/**/.git are also mounted read-only.
To mount extra endpoints, use RO_BIND= and RW_BIND= environment variables.
Anything else not explicitly mounted is either not there or
lost upon namespace termination.
File $PWD/.env (dotenv)
is respected, sourced, and exported to the sandbox environment.
The following environment variables can be set to influence program behavior:
ROOT=Path to sandbox root filesystem (default:$PWD/.sandbox).RO_BIND=,RW_BIND=Extra mount points to bind-mount read-only (or read-write respectively) inside the sandbox. Space- or newline-delimited (useful if argument paths themselves contain spaces). If any argument is likesrc:dst, pathsrcis mounted asdstinside the sandbox. These variables also support glob wildcard patterns.PORTS=Space- or comma-separated list of ports to forward from host to guest. Format like for Docker/podman-pswitch:host_port:guest_port[/protocol]. Example:PORTS=8080:8080,8123:123/udp. This variable has no effect if host networking namespace is shared (i.e.slirp4netnsis unavailable). See section Networking below.SLIRP4NETNS_ARGS=Extra arguments passed toslirp4netnsbinary. Can be used especially to pass--disable-host-loopbackand thus prevent guest-to-host connections via default 10.0.2.2 gateway.CAPS=List of capabilities to maintain (default:NET_RAW, DAC_OVERRIDE, NET_BIND_SERVICEif run as root,NET_RAWotherwise).DEFAULT_RO_BIND=,DEFAULT_RW_BIND=Override default mount points. Set clear to disable default mounts like/usrand/lib.VERBOSE=Print to stderr verbose debug messages pertaining to sandbox initialization and cleanup.DEBUG=Even more verbose debugging, useful in development of the wrapper script itself.CLEANUP=If set, remove$ROOTafter execution.
Symlinks to sandbox-run are resolved, e.g.:
ln -s /usr/local/bin/npm "$(which sandbox-run)"
which npm # Confirm npm points to sandbox-run
# Now `npm` runs inside the sandbox everywhere
npm -vIf environment variable VERBOSE= is set to a non-empty value,
verbose/debug program output is emitted to stderr upon execution.
You can list sandboxed process namespaces using the
command lsns
or the following shell wrapper:
list_sandbox_namespaces () {
lsns -u -W | {
IFS= read header; echo "$header"
grep --color=never "sandbox-run|slirp4netns"
}
}
list_sandbox_namespaces # Function callYou can run sandbox-run without arguments to spawn an interactive shell.
When the filter file exists, seccomp filtering is set up using:
setpriv --seccomp-filter="$ROOT/seccomp_filter.bin" ...Default filtering is automatically set up if enosys is available
(package util-linux-extra on Debian/Ubuntu).
Most syscalls are allowed by default, but the dangerous ones are filtered out,
including all the
syscalls blocked by Docker.
sudo apt install util-linux-extra # For enosys
# Optionally generate custom seccomp filter file
enosyss --dump='$PWD/.sandbox/seccomp_filter.bin' --syscall ...Firejail profile in $ROOT/firejail.profile is read,
As this program is only a rudimentary Firejail approximation,
only directives include, noblacklist, read-only are interpreted.
To see what's failing, run the sandbox with something like
colorstrace:
sandbox-run colorstrace -f -e '%file,%process' my-failing-progNetwork connection is enabled via a shared net namespace or,
preferably, via a
slirp4netns
bridge when available.
Set PORTS= environment variable to enable port forwarding from host to guest.
Connections from guest to host (via 10.0.2.2) are enabled by default.
Set SLIRP4NETNS_ARGS=--disable-host-loopback to disable this gateway.
See section Environment variables above.
To pass extra environment variables, other than those filtered by default,
use an .env file:
echo 'OPENAI_API_KEY=1111111111' > .env
sandbox-run env | grep API_KEY # Verify the variables are set
sandbox-run my-ai-progTo run the sandboxed process as superuser
(while still retaining most of the security functionality of the container sandbox),
e.g. to open privileged ports, simply use sudo:
sudo sandbox-run python -m http.server 80If extra capabilities are required:
CAPS=+SYS_PTRACE sandbox-run strace -f malware
CAPS=+SYS_ADMIN sandbox-run unshare --net idTo run GUI (X11) apps, some prior success was achieved using e.g.:
RO_BIND='/tmp/.X11-unix/X0' sandbox-run xtermUse NVIDIA for CUDA/compute by exposing the various device handles and forwarding the relevant port:
RW_BIND='/dev/nvidia*' \
PORTS=8080:8080 \
sandbox-run llama-server -m Qwen3Hide files inside the sandbox by bind-mounting /dev/null over them. Hide directories by bind-mounting empty directories over them.
empty_dir="$(mktemp -d)"
RO_BIND="/dev/null:$PWD/hidden/file $empty_dir:$PWD/hidden/dir" \
sandbox-run ... # Won't see real $PWD/hidden/{file,dir}You see a mistake—you fix it. Thanks!
See a few alternatives discussed over at sister project
sandbox-venv.