Skip to content
github-actions[bot] edited this page Oct 10, 2026 · 6 revisions

Bubblewrap (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.

Solution in brief

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).

Advantages

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.

Disadvantages

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.

How it works

  1. Host: The daemon loads the bwrap template from pysandboxes/templates/bwrap.template, applies env vars, then builds the command: template options + --setenv from rules, then network:
    • --share-net only if you force it with bwrap.share-net=yes, or if isolation is disabled with bwrap.unshare-net=no (or 0 / false).
    • --unshare-net otherwise (see Configuration parameters): loopback only without socket rules, slirp4netns + iptables with them.
  2. Optional bwrap.* overrides: Other bwrap.<option>=<value> lines become --<option>=<value> (or flag-only if value is empty). share-net and unshare-net are handled by the logic above and are not passed through as duplicate flags.
  3. Mounts: Read-only bubblewrap binds for /usr, /etc, the real path /etc/resolv.conf resolves to when it is a symlink into /run (e.g. systemd-resolved's stub-resolv.conf) -- /run itself is never bound, which would also expose /run/docker.sock and every /run/user/<uid> -- /lib//lib64, the resolved Python executable chain, sys.path and site-packages; then expose-ro before expose-rw from file rules, temp dir for the config pipe, and placeholder mounts for ignore rules (same idea as unshare).
  4. 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.
  5. When --unshare-net is 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, with nsenter --preserve-credentials into 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 in pysandboxes/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 host 127.0.0.1 into the namespace via the slirp API.
  6. Inside the sandbox: main_sandbox loads the config, applies Python guards, starts the SSE server on the expected port and waits for requests.
  7. 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.

Using with Docker

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-bwrap

Then 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'

Using with Podman

Use the same provider image as for Docker:

make build-image-bwrap
podman \
  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'

Using with Kubernetes

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.

Configuration parameters

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 (or 0 / false): disable the automatic --unshare-net; same effect as bwrap.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.

Prerequisites

  • The bwrap binary (e.g. sudo apt install bubblewrap).
  • slirp4netns, nsenter and iptables when you use socket rules without bwrap.share-net=yes (e.g. sudo apt install slirp4netns util-linux iptables).

Recommendations

  • Keep the default isolated network namespace. Use bwrap.share-net=yes only when you accept that the sandbox sees the whole host network, with no socket-level OS filtering.
  • Install slirp4netns as 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: host is often needed with --share-net.

References

Clone this wiki locally