Repository navigation
bwrap
Bubblewrap (bwrap) is an OS-sandbox provider that runs code in Linux namespaces with bind mounts and optional user namespace. The sandbox always gets its own network namespace (--unshare-net): without socket rules it only has loopback (default-deny). With socket rules, the daemon also starts slirp4netns on the host, and applies iptables inside the namespace so outbound traffic is filtered—similar in spirit to the unshare provider, but still under a single bwrap process tree. See the bwrap documentation for details.
The host runs bwrap with options from a template (e.g. --clearenv) plus --unshare-net, or --share-net when a bwrap.* override asks for it. It translates expose-ro / expose-rw file rules into bubblewrap bind mounts, plus minimal system binds (/usr, /etc, the real path /etc/resolv.conf resolves to when it is a symlink into /run, /lib//lib64, Python venv and sys.path). /run itself is never bound: it would also expose /run/docker.sock and every /run/user/<uid>, including another user's session D-Bus bus. The sandbox runs python -m pysandboxes.remote.main_sandbox with config from a named pipe. The host talks to the sandbox via SSE on http://localhost:PORT (slirp4netns maps host loopback to the guest when a separate netns is used).
| Aspect | Detail |
|---|---|
| Isolation | Mount namespace, plus the user namespace bwrap creates on its own when run unprivileged, and a network namespace unless bwrap.share-net=yes. The --unshare-* options of bwrap.template are commented out, so IPC, PID and UTS stay shared with the host unless enabled with bwrap.unshare-ipc= (etc.). Filesystem restricted to explicit expose rules (implemented as bind mounts) and ignore rules. |
| Network modes | Isolated netns by default: loopback only without socket rules, user-land filtering when socket rules are present. Shared host network only on explicit opt-out (bwrap.share-net=yes). |
| Lightweight | Single child process from bubblewrap’s perspective; no VM; fast startup. |
| Alignment with other providers | Same API (SSE, call_in_sandbox) and config flow (named pipe) as firejail/unshare; slirp/port-forward logic is shared with unshare via slirp4netns_common. |
| Aspect | Detail |
|---|---|
| Network | With socket rules, you depend on slirp4netns, and on nsenter and iptables-restore on the host, which loads the filter: when it cannot be loaded, or the network never comes up, the sandbox refuses to start rather than run unfiltered. Opting out with bwrap.share-net=yes gives the sandbox the whole host network. |
| User id | With socket rules, the sandboxed program runs as uid 0 of its own user namespace (--unshare-user --uid 0 --gid 0), still the caller's uid on the host and with no capability: files it creates are the caller's. Without socket rules, it keeps the caller's uid. |
| Containers | In Docker/Podman, bridge networking may still block reachability to the sandbox SSE port; network_mode: host is often needed when using --share-net. |
| Dependencies | Requires the bwrap binary. If you use socket rules (isolated netns path), slirp4netns, nsenter (util-linux) and iptables must also be installed. |
-
Host: The daemon loads the bwrap template from
pysandboxes/templates/bwrap.template, applies env vars, then builds the command: template options +--setenvfrom rules, then network:-
--share-netonly if you force it withbwrap.share-net=yes, or if isolation is disabled withbwrap.unshare-net=no(or0/false). -
--unshare-netotherwise (see Configuration parameters): loopback only without socket rules, slirp4netns + iptables with them.
-
-
Optional
bwrap.*overrides: Otherbwrap.<option>=<value>lines become--<option>=<value>(or flag-only if value is empty).share-netandunshare-netare handled by the logic above and are not passed through as duplicate flags. -
Mounts: Read-only bubblewrap binds for
/usr,/etc, the real path/etc/resolv.confresolves to when it is a symlink into/run(e.g. systemd-resolved'sstub-resolv.conf) --/runitself is never bound, which would also expose/run/docker.sockand every/run/user/<uid>--/lib//lib64, the resolved Python executable chain,sys.pathand site-packages; thenexpose-robeforeexpose-rwfrom file rules, temp dir for the config pipe, and placeholder mounts for ignore rules (same idea as unshare). -
Launch: The host runs
bwrap [args] -- python -m pysandboxes.remote.main_sandbox --_named-pipe <path>. The config is written to the named pipe; the child reads it on startup. -
When
--unshare-netis used with socket rules: bwrap itself stays in the host namespaces to monitor its child, so the host first waits for that child to sit in its own network namespace. It then loads the iptables rules derived from socket rules from the host, withnsenter --preserve-credentialsinto the sandbox user and network namespaces, where its uid is mapped to root (DNS is expected at the usual slirp address 10.0.2.3; inbound SSE from the host gateway 10.0.2.2 is allowed). The sandbox holds no capability, so it can neither load these rules nor flush them. Only then does a background thread on the host run slirp4netns against the child (shared helpers inpysandboxes/remote/slirp4netns_common.py), and the host waits for it to be ready. The child stays blocked on its config pipe all along: no sandboxed code runs before the filter and the network are in place. TCP/UDP ports from inbound ALLOW rules (plus the SSE port) can be forwarded from host127.0.0.1into the namespace via the slirp API. -
Inside the sandbox:
main_sandboxloads the config, applies Python guards, starts the SSE server on the expected port and waits for requests. -
Communication: The host uses
http://localhost:PORT; with slirp, connections to host loopback are forwarded to the guest.
For more details, see the bubblewrap documentation.
The bwrap provider can run inside Docker. The project uses one image per OS provider: the image for bwrap is python-sb-bwrap (tagged e.g. :latest or :3.12). Build it from the project root with:
make build-image-bwrapThen run with the code mounted and the provider image. With --share-net, the sandbox uses the container’s network; ensure the host can reach the SSE port (e.g. publish the port or use network_mode: host where appropriate). If your profile uses socket rules, the image must include slirp4netns (and iptables as needed inside the namespace).
docker \
run -it --rm \
--privileged \
-v "$(pwd)":/app \
-w /app \
python-sb-bwrap:latest \
sh -c 'pip install -e . && OS_SANDBOX=bwrap python-sb -m tests.integration_tests.tst_usage'Use the same provider image as for Docker:
make build-image-bwrappodman \
run -it --rm \
--privileged \
-v "$(pwd)":/app \
-w /app \
python-sb-bwrap:latest \
sh -c 'pip install -e . && OS_SANDBOX=bwrap python-sb -m tests.integration_tests.tst_usage'Use the provider image python-sb-bwrap:latest (built with make build-image-bwrap). For minikube, build the images on the host and load them into the cluster: make minikube-build-images (runs make build-images, then minikube image load for each image). For this provider only: make build-image-bwrap then minikube image load python-sb-bwrap:latest.
Example Pod: same structure as for unshare (see unshare.md), with image: python-sb-bwrap:latest and the same volume mount for /app. Network and capabilities depend on whether you use socket rules (slirp + netns), no socket rule (netns with loopback only) or bwrap.share-net=yes (shared network); adjust securityContext (e.g. privileged: true or capabilities SYS_ADMIN, NET_ADMIN) as required by your cluster.
You can add bwrap-specific options in .py-sandboxes. Any line of the form bwrap.<option>=<value> is interpreted as follows:
-
bwrap.share-net=yes(or any non-empty value): use--share-net, with or without socket rules: the sandbox sees the host network. Socket-level OS filtering is then not applied; install slirp4netns is not required for that mode. -
bwrap.unshare-net=no(or0/false): disable the automatic--unshare-net; same effect asbwrap.share-net=yes. -
Any other
bwrap.<option>: passed to bwrap as--<option>=<value>or--<option>if the value is empty.
If socket rules require the isolated network path and slirp4netns, nsenter or iptables-restore is missing, the daemon exits with an error suggesting installation or bwrap.share-net=yes.
- The
bwrapbinary (e.g.sudo apt install bubblewrap). -
slirp4netns,nsenterandiptableswhen you use socket rules withoutbwrap.share-net=yes(e.g.sudo apt install slirp4netns util-linux iptables).
- Keep the default isolated network namespace. Use
bwrap.share-net=yesonly when you accept that the sandbox sees the whole host network, with no socket-level OS filtering. - Install
slirp4netnsas soon as your profile carries socket rules: without it, the daemon exits. - Enable the IPC, PID or UTS namespaces explicitly when you need them: the template leaves them shared with the host.
- In a container, make sure the host can reach the SSE port. Note that
network_mode: hostis often needed with--share-net.