Skip to content

Instantly share code, notes, and snippets.

@dgalli1
Last active June 23, 2026 12:56
Show Gist options
  • Select an option

  • Save dgalli1/70c3fcb623ccd53521e53b88acd659a0 to your computer and use it in GitHub Desktop.

Select an option

Save dgalli1/70c3fcb623ccd53521e53b88acd659a0 to your computer and use it in GitHub Desktop.
LLM Sandbox Bubblewrap with Podman support
#!/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