Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down Expand Up @@ -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)
Expand All @@ -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/<distro>/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
Expand Down
63 changes: 40 additions & 23 deletions src/project_wrap/templates/project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ~/
Expand Down
Loading