How to Sandbox a Script With bubblewrap or firejail
Sandbox an untrusted script with bubblewrap or firejail: read only system, private home, no network, die with parent, and the limits of user namespaces.

Running a script you did not write, a downloaded installer, a build step from a stranger's repository, means giving it your user's full access: your SSH keys, your browser profile, the network. A sandbox takes most of that away without a virtual machine. Linux offers it through namespaces, and two tools make them usable from the command line. This post shows bubblewrap and firejail, how they differ and where they stop. It extends seccomp and namespaces and the lessons in curl pipe bash risk. The commands assume bubblewrap 0.10 and firejail 0.9.72.
Bubblewrap
bwrap \
--ro-bind /usr /usr \
--symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--proc /proc --dev /dev \
--tmpfs /tmp --tmpfs /home \
--ro-bind ./install.sh /work/install.sh \
--chdir /work \
--unshare-all --die-with-parent \
/bin/sh /work/install.shThe sandbox starts empty. Only what you bind into it exists: the system binaries read-only, the script itself read-only, a private memory-backed /tmp and /home that disappear at exit. --unshare-all creates new namespaces for processes, IPC, UTS, cgroups, mounts and the network, so the script has no network and cannot see other processes. --die-with-parent ends the sandbox if the shell that started it exits. Your home directory, with its keys and tokens, is not visible at all.
Give back only what is needed
bwrap --ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--proc /proc --dev /dev --tmpfs /tmp \
--bind "$PWD/out" /out \
--share-net --ro-bind /etc/resolv.conf /etc/resolv.conf --ro-bind /etc/ssl /etc/ssl \
--unshare-pid --die-with-parent \
python3 /usr/lib/mytool/fetch.pyWritable access is limited to ./out. Network access is added back with --share-net, with DNS and certificate files, for a tool that must download. Start with nothing, run the script, read the errors and add the missing mounts. The error messages (No such file or directory, Temporary failure in name resolution) are your to-do list.
Firejail
firejail --net=none --private --private-tmp --noroot --caps.drop=all --seccomp ./install.sh
firejail --read-only=/home --whitelist=~/Downloads firefoxFirejail wraps the same kernel features behind high-level options and ships profiles for hundreds of desktop programs. --private gives the program a fresh, empty home; --net=none removes the network; --seccomp applies a system call filter. Where bubblewrap starts empty and you add, firejail starts from the host and you take away, which is quicker for known applications and easier to get wrong for unknown ones.
Trade-offs
- Firejail is installed setuid root, so a bug in it is a privilege escalation on the host; several have been found. Bubblewrap uses unprivileged user namespaces and no setuid, a smaller attack surface, and it is what Flatpak builds on.
- Ubuntu 24.04 restricts unprivileged user namespaces with AppArmor, and bwrap fails with
setting up uid map: Permission denieduntil an AppArmor profile allows it; thebubblewrappackage ships one for the standard path. Some distributions disable the feature by sysctl (kernel.unprivileged_userns_clone). - Both share the host kernel. A kernel exploit inside the sandbox escapes it.
Test the walls
bwrap --ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--proc /proc --dev /dev --tmpfs /tmp --unshare-all /bin/sh -c 'ls ~ ; ping -c1 1.1.1.1; touch /usr/x'Try to break out on purpose: list the home directory, reach the network, write to the read-only mount. Each should fail. A sandbox you have not tested is an assumption.
Where this does not apply
For malware analysis or running code that is truly hostile, use a disposable virtual machine, which has a much smaller interface to the host. Sandboxes also do not stop a script from burning CPU or filling memory unless you add cgroup limits, as in capping runaway jobs with cgroups. For application services, hardened containers are the usual unit. The better habit is reading a script before running it.
How the sandbox is built
Both tools use features that the Linux kernel provides without a hypervisor. Namespaces give a process its own view of the system: its own mount points, so it sees only the directories you give it, its own network, so it can have no connectivity at all, its own process list and its own user IDs. Control groups can limit resources, and a seccomp filter can block system calls the program should never make. Bubblewrap is a small tool that sets these up from the command line, and it is the basis of Flatpak's sandboxing. A typical invocation starts with a read-only view of the system directories, a fresh /tmp that is empty, a private process list and no network, and then adds back only the specific paths and capabilities the program needs. Because it uses unprivileged user namespaces, it runs as a normal user on most current distributions, though some, in hardened configurations, restrict that feature and the tool then fails with an error about permissions.
The kernel is shared. A sandbox built from namespaces is not a virtual machine, and a kernel bug can lead out of it. It is strong against mistakes and common malware and weaker against a determined attacker with a kernel exploit.
Choosing between the two
Firejail takes the other approach: it ships many ready-made profiles for common programs and applies them with one word, such as firejail firefox, with options like --net=none to cut networking, --private to give a throwaway home directory and --read-only to make paths unwritable. That is convenient for desktop applications and quick experiments. The tool is installed as a setuid root program so that it can set up the sandbox, and a setuid binary that parses complex input is a large attack surface, which is why Firejail has had several privilege escalation vulnerabilities over the years. Bubblewrap is smaller and does less, so there is less to attack, and you write the arguments yourself. A reasonable rule is to prefer bubblewrap for scripts and automation where you control the arguments, use Firejail's profiles when convenience matters and keep it updated, and treat either as a layer and not a guarantee.
Giving back only what is needed
Design the sandbox by starting from nothing and adding. Mount the system directories read-only, give the script a working directory with write access and nothing else, and leave out your home directory, so the script cannot read SSH keys, browser data or credentials. Cut the network unless the script needs to download something, and then consider a limited alternative such as a proxy. Drop all capabilities, since the sandbox does not need them, and set the option that kills the sandboxed process when the parent exits, so nothing lingers. Each exception you add should have a reason you can state. When the program fails, read the error and add the single path or permission it complains about, and stop there. A sandbox that gradually accumulates every permission the program asks for ends as no sandbox at all, so keep the list short and review it from time to time.
Test the walls from inside. Try to read a file you excluded, list another user's processes, open a network connection and write to a read-only path, and confirm that each fails. A sandbox you have not attacked is a sandbox you are hoping works.
See the bubblewrap project and Firejail documentation.


