Skip to content
Open
57 changes: 54 additions & 3 deletions docs/bwrap-support/bubblewrap-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ requiring root privileges or a container runtime.
# Alpine
apk add bubblewrap
```
The deny-by-default baseline (see [How It Works](#how-it-works)) emits its
read-only mounts via `--ro-bind-try`, which requires **bwrap 0.3.0+**
(released 2017; every currently-supported distro ships a newer version).
- User namespaces must be enabled:
```bash
# Check: should print "1"
Expand Down Expand Up @@ -54,7 +57,12 @@ lxc-exec --experimental --config-base64 "$(base64 -w0 bubblewrap_hello.json)"
Bubblewrap creates a namespace-isolated process by:

1. Unsharing user, PID, IPC, and UTS namespaces (`--unshare-*`)
2. Bind-mounting the host root filesystem read-only as a base
2. Bind-mounting a **minimal deny-by-default baseline** read-only into the
sandbox (`/bin`, `/sbin`, `/lib*`, `/usr/bin`, `/usr/sbin`, `/usr/lib*`,
`/usr/libexec`, `/usr/share`, `/etc`, plus DNS stub-resolver dirs
under `/run`). Everything else on the host β€” including the caller's
`$HOME`, `/root`, `/opt`, `/var`, `/sys`, and `/run/user/<uid>` β€” is
invisible inside the sandbox.
3. Layering filesystem policy overrides (read-write, read-only, denied paths)
4. Setting up minimal `/dev`, `/proc`, and `/tmp`
5. Clearing the environment and applying only requested variables
Expand All @@ -63,6 +71,45 @@ Bubblewrap creates a namespace-isolated process by:
The sandboxed process runs as a child of `bwrap` and dies automatically when
execution completes β€” no container lifecycle management required.

### Deny-by-default filesystem

The baseline mirrors the macOS Seatbelt backend's `(deny default)` posture:
the sandbox can read the dynamic linker, libc, system tools, and system
configuration β€” and **nothing else** β€” until the caller opts in via
`readonlyPaths` / `readwritePaths`. To make a host directory visible inside
the sandbox, list it explicitly:

```json
{
"filesystem": {
"readonlyPaths": ["/home/alice/project", "/usr/local"],
"readwritePaths": ["/tmp/workspace"]
}
}
```

Common consequences of this default:

- `$HOME` (e.g. `~/.aws/credentials`, `~/.ssh/id_*`, browser cookies) is
not readable from the sandbox.
- `/opt` and `/usr/local` tooling is not on PATH; list either path under
`readonlyPaths` if the script depends on it.
- `working_directory` must live under the baseline or a policy path β€” a
`cwd` of `~/project` without a matching `readonlyPaths` entry will fail.
- DNS works on systemd-resolved, NetworkManager, and resolvconf hosts
because the corresponding `/run/...` directories are bound. The common
symlink targets *outside* `/run` are covered too: `/var/run/...`-routed
`/etc/resolv.conf` symlinks resolve via a synthesised `/var/run -> /run`
compat symlink, and WSL's `/mnt/wsl/resolv.conf` is bound directly.
Neither exposes host `/var` or `/mnt` contents. Hosts that point
`/etc/resolv.conf` at some other custom location still need that target
listed in `readonlyPaths`.

Comment thread
caarlos0 marked this conversation as resolved.
Files in `/etc` that contain secrets (`/etc/shadow`, `/etc/sudoers`,
Comment thread
caarlos0 marked this conversation as resolved.
`/etc/ssh/ssh_host_*_key`) are mode `0400` / `0640` `root` and remain
unreadable to a non-root caller β€” user-namespace UID mapping does not
bypass kernel DAC.

## Configuration

Bubblewrap uses the shared cross-backend configuration fields. No
Expand Down Expand Up @@ -291,8 +338,12 @@ Test configs are in `tests/configs/bubblewrap_*.json`.

- **Experimental** β€” requires `--experimental` flag
- **Linux only** β€” Bubblewrap requires Linux kernel namespaces
- **Host filesystem** β€” the sandbox sees the host's files (read-only by
default); there is no separate rootfs
- **Deny-by-default filesystem** β€” the sandbox sees a minimal allowlist
of host paths (system binaries, libs, `/etc`, DNS stub-resolver dirs)
and nothing else. `$HOME`, `/opt`, `/var`, `/sys`, `/run/user/<uid>`,
and `/usr/local` are invisible unless explicitly listed in
`readonlyPaths` / `readwritePaths`. There is no separate rootfs β€” the
visible paths are bind-mounted from the host.
- **Network filtering** β€” per-host `allowedHosts`/`blockedHosts` is best
done via the cooperative env-var **network proxy** (no privilege
required, see above). The legacy iptables path
Expand Down
Loading
Loading