From 5028971ef4f0286faedc5e10fd8ba6b862e42a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=A5=C3=A5rd?= Date: Wed, 22 Apr 2026 18:27:43 +0200 Subject: [PATCH] Default template to deny-by-default home plus clean_env --- CHANGELOG.md | 14 ++++++ README.md | 47 +++++++++++++----- src/project_wrap/templates/project.toml | 63 ++++++++++++++++--------- 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ebead..f0bf177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,20 @@ whitelisting `/mnt/wsl` (e.g. to get resolv.conf) had a clean path to the host Docker engine — reachable via `curl --unix-socket …` — which is a full root escape. Candidate list now accepts glob patterns — `core.py` +- Default `pwrap --new` template now uses deny-by-default home + (`blacklist = ["~", "/mnt"]`) with a narrow `whitelist` for shell config + and `~/.gitconfig`. Previously the template enumerated credential dirs + (`.ssh`, `.aws`, `.gnupg`, ...) which rotted as new tools added new + credential files (`.claude.json`, `.boto`, `.config/op`, + `.config/pypoetry`, `.config/uv` were all readable on a vanilla install). + Template-only; existing configs unaffected — `templates/project.toml` +- Default template now sets `clean_env = true`. Without it, host env leaks + into the sandbox (a probe caught `ANTHROPIC_API_KEY` and `CODESTRAL_API_KEY` + passing through). Template-only — `templates/project.toml` +- README gains a "WSL users" subsection in Security Defaults explaining that + `/etc/resolv.conf` symlinks into `/mnt/wsl` and recommending a narrow + `whitelist = ["/mnt/wsl/resolv.conf"]` instead of the whole tree (which + contains the Docker Desktop engine socket) ## 202604.4 diff --git a/README.md b/README.md index 61ded5e..ecd2640 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,14 @@ shell = "/usr/bin/fish" [sandbox] enabled = true blacklist = [ # can't be accessed at all from the sandbox - "~/.kube", - "~/.aws", - "~/.ssh", - "~/projects/", # hide all other projects - # trimmed for brevity — see the generated template (`pwrap --new`) for - # the full default blacklist (GCP, Azure, GPG, docker, npm, PyPI, /mnt). + "~", # deny-by-default home (default template) + "/mnt", # WSL Windows drives (default template) + "~/projects/", # hide all other projects ] -whitelist = [ # exceptions to the blacklist - "~/.kube/myproject", +whitelist = [ # read-only exceptions to the blacklist + "~/.config/fish", # shell config + "~/.gitconfig", + "~/.kube/myproject", # per-project kubeconfig "~/.ssh/myproject_ed25519", "~/.ssh/known_hosts", ] @@ -319,14 +318,20 @@ pwrap --version # show version When sandboxing is enabled: -- Home is **read-only**; only the project directory is writable +- Home is **bound read-only**; only the project directory is writable - Config directory (`~/.config/pwrap`) is always blacklisted - Docker sockets masked at `/run/docker.sock`, `/var/run/docker.sock`, - `~/.docker/desktop/docker-cli.sock`, `~/.docker/run/docker.sock` — + `~/.docker/desktop/docker-cli.sock`, `~/.docker/run/docker.sock`, and the + WSL Docker Desktop paths under `/mnt/wsl/docker-desktop*` — `connect()` works on ro-bound sockets, so an exposed docker socket is a full escape to root. Override via `writable` to enable docker access. -- Default template blacklists credential dirs (SSH, GPG, AWS, GCP, Azure, - Docker, npm, PyPI) and `/mnt` (WSL Windows drives) +- Default template uses **deny-by-default home**: `blacklist = ["~", "/mnt"]` + hides every dotfile and WSL Windows drives, and `whitelist` binds shell + config and `~/.gitconfig` back read-only. Add paths to `whitelist` (ro) + or `writable` (rw) for what your project needs +- Default template sets `clean_env = true` — host env is cleared except for + `PATH/HOME/USER/SHELL/TERM/LANG`; pass others through with `[env]` to + prevent accidental leakage of `ANTHROPIC_API_KEY`, `AWS_*`, `GH_TOKEN`, etc. - PID and IPC namespaces are isolated - TIOCSTI injection blocked automatically on kernels < 6.2 - XDG runtime directory isolated (D-Bus, Wayland, keyring sockets) @@ -335,6 +340,24 @@ When sandboxing is enabled: - Writable and blacklist paths must exist on the host; missing entries abort with a single aggregated error listing every missing path +##### WSL users ##### + +`/etc/resolv.conf` on WSL is a symlink into `/mnt/wsl`, so blacklisting +`/mnt` breaks DNS inside the sandbox. Fix it with a **narrow** whitelist: + +```toml +whitelist = [ + "/mnt/wsl/resolv.conf", +] +``` + +**Do not** whitelist the whole `/mnt/wsl` tree — it contains the Docker +Desktop engine socket at `/mnt/wsl/docker-desktop-bind-mounts//docker.sock` +and various `shared-sockets/*.sock`, all reachable with `curl --unix-socket` +once they're visible inside the sandbox. A reachable docker socket is a full +root escape. The socket mask covers these paths by default, but only if +you don't re-expose them writable via `/mnt/wsl`. + Run your editor from inside the sandbox if it has any capacity to run linters, hooks, or anything else from the project environment. A super-protected terminal does nothing if a malicious `.pth` can escape diff --git a/src/project_wrap/templates/project.toml b/src/project_wrap/templates/project.toml index fac1315..d3bfe42 100644 --- a/src/project_wrap/templates/project.toml +++ b/src/project_wrap/templates/project.toml @@ -7,28 +7,43 @@ shell = "{shell}" enabled = {sandbox_enabled} # Note: the config directory (~/.config/pwrap) is always blacklisted automatically. blacklist = [ # Paths to hide (overlaid with tmpfs) - "~/.ssh", # SSH keys and agent config - "~/.gnupg", # GPG keys - "~/.aws", # AWS credentials and config - "~/.kube", # Kubernetes contexts and tokens - "~/.config/gcloud", # GCP credentials and auth tokens - "~/.azure", # Azure CLI credentials - "~/.docker", # Docker config and auth tokens - "~/.npmrc", # npm auth tokens - "~/.pypirc", # PyPI upload tokens - "~/.boto", # Legacy GCS/S3 credentials - "/mnt", # WSL: Windows drives. Without this, cmd.exe and - # powershell.exe are callable via binfmt_misc - # interop — a full sandbox escape. + "~", # Deny-by-default home. Removing this + # re-exposes every host credential file + # (SSH, AWS, GCP, Claude, shell history, ...) + # — whitelist specific paths back below. + "/mnt", # WSL: Windows drives. Without this, cmd.exe + # and powershell.exe are callable via + # binfmt_misc interop — a full sandbox escape. ] -# whitelist = [ # Exceptions to blacklist (bound back read-only) -# "~/.kube/{name}", -# "/mnt/wsl", # WSL config — /etc/resolv.conf symlinks here, -# # so DNS breaks without it when /mnt is blacklisted. -# # Add "/mnt/wslg/..." writable for GUI apps. -# ] -# writable = [ # Extra writable paths (home is read-only). -# "~/.pyenv/shims", +whitelist = [ # Read-only exceptions to the blacklist + # Shell config — pick the one(s) your shell reads: + "~/.config/fish", # fish: config + completions + functions + # "~/.bashrc", # bash + # "~/.zshrc", # zsh + + # Git config (most projects need it for commits/auth): + "~/.gitconfig", + + # WSL DNS (only when on WSL and /mnt is blacklisted). Narrow to the one + # file /etc/resolv.conf symlinks to — whitelisting all of /mnt/wsl also + # exposes Docker Desktop sockets (reachable via curl --unix-socket): + # "/mnt/wsl/resolv.conf", +] +# writable = [ # Extra writable paths (home is hidden/ro) +# # Prefer `whitelist` (ro) for anything you only need to read. Examples +# # of legitimate rw needs: +# +# # Shell history — writable so new commands persist to the host. +# # Caveat: history files accumulate pasted secrets; omit for higher-risk +# # workloads. +# # "~/.local/share/fish/fish_history", +# # "~/.bash_history", +# # "~/.zsh_history", +# +# # Project Python tooling (pyenv shims are read-execute, not read-write; +# # use whitelist unless something actually needs to write here): +# # "~/.pyenv/shims", +# # # To enable docker inside the sandbox, uncomment the matching socket # # below (this overrides the default docker-socket mask): # # "/run/docker.sock", # system docker @@ -41,8 +56,10 @@ blacklist = [ # Paths to hide (overlaid with tmpfs) # unshare_net = false # Isolate network namespace # unshare_pid = true # Isolate PID namespace (default: true) # new_session = true # TIOCSTI protection (auto on kernels < 6.2) -# clean_env = false # Clear env, pass through only PATH/HOME/USER/ -# # SHELL/TERM/LANG. Add others via [env]. +clean_env = true # Clear env, pass through only PATH/HOME/USER/ + # SHELL/TERM/LANG. Add others via [env]. + # Without this, host ANTHROPIC_API_KEY / + # AWS_* / GH_TOKEN etc. leak into the sandbox. # [env] # environment variables (set before shell starts) # XDG_DATA_HOME = "vault/.config" # ~ expanded for values starting with ~/