Last active
June 23, 2026 12:56
-
-
Save dgalli1/70c3fcb623ccd53521e53b88acd659a0 to your computer and use it in GitHub Desktop.
LLM Sandbox Bubblewrap with Podman support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Rootless Podman inside a bubblewrap sandbox. | |
| # ------------------------------------------------------------------------------ | |
| # WHY PODMAN AND NOT DOCKER | |
| # Docker needs a privileged daemon (dockerd). Rootless-docker still wants its | |
| # own daemon + slirp4netns and is painful to nest. Podman is daemonless and | |
| # rootless, so it is the realistic thing to run inside bwrap. | |
| # | |
| # THE TRICKS THAT MAKE IT WORK INSIDE A USER NAMESPACE | |
| # * We are ALREADY in one user namespace (bwrap creates it). newuidmap / | |
| # newgidmap cannot lay down the /etc/subuid range from in here (no-new-privs | |
| # blocks the setuid binary and only a single uid is mapped in the parent). | |
| # -> Force Podman into SINGLE-UID rootless mode by masking /etc/subuid and | |
| # /etc/subgid with empty files. Container UID 0 == your host UID. | |
| # ignore_chown_errors lets images that reference other uids still unpack. | |
| # * No fuse-overlayfs available -> use the "vfs" storage driver (no fuse, no | |
| # special mounts; copies layers, so it is slower and uses more disk). | |
| # * No writable cgroup delegation inside bwrap -> run containers with cgroups | |
| # disabled (cgroup_manager only matters if you later add crun + delegation). | |
| # * Networking via pasta (slirp4netns is absent). pasta needs /dev/net/tun, | |
| # which we bind in explicitly because --dev /dev hides it. | |
| # | |
| # LIMITATIONS / FALLBACKS | |
| # * vfs is disk-hungry and slow; fine for CLI/build use, bad for huge images. | |
| # * Single-UID: containers that genuinely need distinct uids (some DBs that | |
| # drop privileges) may misbehave. Most tooling/build use cases are fine. | |
| # * If pasta networking misbehaves, run a container with `--network=none`. | |
| # * Installing `crun`, `fuse-overlayfs` and `slirp4netns` on the host would | |
| # let you switch to overlay storage + better isolation later. | |
| # | |
| # USAGE | |
| # ./sandbox-podman.sh # interactive shell with podman ready | |
| # ./sandbox-podman.sh podman info # run one command and exit | |
| # ------------------------------------------------------------------------------ | |
| set -euo pipefail | |
| SANDBOX_ID=$(printf '%s' "$PWD" | sha256sum | cut -d' ' -f1) | |
| # Persistent bits (image store, volumes, runroot) live on real disk, NOT on the | |
| # tmpfs-backed /tmp. Only ephemeral bits (true /tmp, transient staging) use | |
| # tmpfs. Per-project isolation is preserved by hashing the project path. | |
| PERSIST_ROOT="${SANDBOX_PERSIST_ROOT:-$HOME/.local/sandbox-shared/$SANDBOX_ID}" | |
| EPHEMERAL_ROOT="${SANDBOX_EPHEMERAL_ROOT:-/tmp/sandbox-shared/$SANDBOX_ID}" | |
| SHARED_ROOT="$PERSIST_ROOT" # backwards-compat var used below | |
| TMPFS_ROOT="$EPHEMERAL_ROOT" # /tmp inside the sandbox | |
| mkdir -p "$PERSIST_ROOT"/{var/lib/containers,var/tmp,work,xdg,conf} | |
| mkdir -p "$TMPFS_ROOT"/tmp | |
| # Symlink the truly-ephemeral dirs onto tmpfs while keeping the persistent | |
| # layout under ~/.local/sandbox-shared. Cheap, no copy on every run. | |
| mkdir -p "$PERSIST_ROOT/var/lib/containers" | |
| chmod 1777 "$PERSIST_ROOT/var/tmp" | |
| chmod 700 "$PERSIST_ROOT/xdg" | |
| # Empty file used to mask /etc/subuid + /etc/subgid -> single-UID podman mode. | |
| : > "$SHARED_ROOT/empty" | |
| UID_NUM=$(id -u) | |
| XDG="/run/user/$UID_NUM" # in-sandbox writable runtime dir (bound below) | |
| CONF_DIR="/sandbox-conf" # writable tmpfs-root path (NOT under ro /etc) | |
| # --- Podman config generated per-project into the shared dir ------------------ | |
| # graphroot lives under /var (writable bind) so images persist between runs. | |
| cat > "$SHARED_ROOT/conf/storage.conf" <<EOF | |
| [storage] | |
| driver = "vfs" | |
| runroot = "$XDG/containers" | |
| graphroot = "/var/lib/containers/storage" | |
| [storage.options.vfs] | |
| ignore_chown_errors = "true" | |
| EOF | |
| cat > "$SHARED_ROOT/conf/containers.conf" <<EOF | |
| [containers] | |
| cgroups = "disabled" | |
| log_driver = "k8s-file" | |
| # Single-UID mode means apt (and anything that drops privileges to a non-root | |
| # uid) would fail with seteuid/setgroups errors. Mount a tiny apt.conf into | |
| # every container telling apt NOT to drop to the _apt user. Harmless / ignored | |
| # in non-apt images. | |
| volumes = [ | |
| "$CONF_DIR/apt-sandbox.conf:/etc/apt/apt.conf.d/99-sandbox-root:ro", | |
| ] | |
| [engine] | |
| cgroup_manager = "cgroupfs" | |
| events_logger = "file" | |
| runtime = "crun" | |
| [network] | |
| default_rootless_network_cmd = "pasta" | |
| EOF | |
| cat > "$SHARED_ROOT/conf/apt-sandbox.conf" <<'EOF' | |
| APT::Sandbox::User "root"; | |
| EOF | |
| cat > "$SHARED_ROOT/conf/registries.conf" <<EOF | |
| unqualified-search-registries = ["docker.io"] | |
| EOF | |
| # rc file auto-loaded inside the sandbox (BASH_ENV for `bash -c`, --rcfile for | |
| # interactive shells). Wraps podman so the benign rootless-in-bwrap log noise is | |
| # hidden WITHOUT swallowing real errors or changing the exit code: | |
| # * --log-level=error drops every WARN/INFO line (single-mapping, missing gids, | |
| # "not a shared mount", systemd pause-process, etc.). | |
| # * the one remaining benign ERRO line (empty /etc/subuid) is filtered by exact | |
| # text on stderr only; everything else passes through untouched. | |
| cat > "$SHARED_ROOT/conf/bashrc" <<'EOF' | |
| # Pull in the user's interactive rc when running interactively (read-only, safe). | |
| if [ -n "${PS1:-}" ] && [ -r "$HOME/.bashrc" ]; then | |
| . "$HOME/.bashrc" 2>/dev/null || true | |
| fi | |
| __sandbox_podman_filter='no subuid ranges found for user|not a shared mount' | |
| podman() { | |
| command podman --log-level=error "$@" \ | |
| 2> >(grep --line-buffered -vE "$__sandbox_podman_filter" >&2) | |
| } | |
| # docker daemon can't run in here; route `docker` to podman so agents that | |
| # reflexively type `docker` still work. | |
| docker() { podman "$@"; } | |
| EOF | |
| # --- Hide sensitive host dirs (same as your original) ------------------------- | |
| HIDE_ARGS=() | |
| for d in .ssh .gnupg .aws .config/gh .cache; do | |
| [[ -e "$HOME/$d" ]] && HIDE_ARGS+=(--tmpfs "$HOME/$d") | |
| done | |
| # --- What to run inside the sandbox ------------------------------------------- | |
| if [[ "$#" -gt 0 ]]; then | |
| # `-c` (not `-lc`) so BASH_ENV is honoured and the podman wrapper loads. | |
| CMD=(/usr/bin/bash -c "$*") | |
| else | |
| CMD=(/usr/bin/bash --rcfile "$CONF_DIR/bashrc" -i) | |
| fi | |
| exec bwrap \ | |
| --tmpfs / \ | |
| --ro-bind /usr /usr \ | |
| --symlink usr/bin /bin \ | |
| --symlink usr/sbin /sbin \ | |
| --symlink usr/lib /lib \ | |
| --symlink usr/lib64 /lib64 \ | |
| --ro-bind /etc /etc \ | |
| --tmpfs /etc/ssh/ssh_config.d \ | |
| --ro-bind /run /run \ | |
| --ro-bind-try /sys /sys \ | |
| --bind "$TMPFS_ROOT/tmp" /tmp \ | |
| --bind "$PERSIST_ROOT/var" /var \ | |
| --bind "$SHARED_ROOT/work" /shared \ | |
| --bind "$HOME/.cache/ms-playwright" "$HOME/.cache/ms-playwright" \ | |
| --ro-bind /srv/miraWWW /srv/miraWWW \ | |
| --ro-bind "$HOME" "$HOME" \ | |
| "${HIDE_ARGS[@]}" \ | |
| --bind "$PWD" "$PWD" \ | |
| --dev /dev \ | |
| --dev-bind-try /dev/net/tun /dev/net/tun \ | |
| --dev-bind-try /dev/fuse /dev/fuse \ | |
| --proc /proc \ | |
| `# --- podman enablement ---` \ | |
| --bind "$SHARED_ROOT/xdg" "$XDG" \ | |
| --ro-bind "$SHARED_ROOT/conf" "$CONF_DIR" \ | |
| --ro-bind "$SHARED_ROOT/empty" /etc/subuid \ | |
| --ro-bind "$SHARED_ROOT/empty" /etc/subgid \ | |
| --setenv XDG_RUNTIME_DIR "$XDG" \ | |
| --setenv BASH_ENV "$CONF_DIR/bashrc" \ | |
| --setenv CONTAINERS_STORAGE_CONF "$CONF_DIR/storage.conf" \ | |
| --setenv CONTAINERS_CONF "$CONF_DIR/containers.conf" \ | |
| --setenv CONTAINERS_REGISTRIES_CONF "$CONF_DIR/registries.conf" \ | |
| --setenv REGISTRY_AUTH_FILE "$XDG/containers/auth.json" \ | |
| --unsetenv SSH_AUTH_SOCK \ | |
| --unshare-pid \ | |
| --unshare-ipc \ | |
| --unshare-uts \ | |
| --hostname "sandbox-$$" \ | |
| "${CMD[@]}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment