From 621bc28f7a446d596ca5fe2d549cf9b1925b1f9e Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 22 Jul 2026 13:03:30 -0700 Subject: [PATCH] claude-sandbox: document SANDBOX_SSH_KEY in Security notes; warn on default-looking personal keys Second-round review follow-up. Security notes was written as the canonical list of residual risks but never mentioned the new SANDBOX_SSH_KEY mechanism, which has the same "signs for any repo it can reach" shape as the forwarded agent (already documented) - add a bullet covering it and the /root-only mount. Also nothing enforced that SANDBOX_SSH_KEY actually be a dedicated low-privilege key rather than a real personal identity, which is the whole basis for accepting it as passphrase-less and left loaded for an entire unattended session. bin/claude-sandbox now warns (non-fatal) if it looks like a default personal keyfile (id_rsa, id_ed25519, etc). --- bin/claude-sandbox | 13 +++++++++++++ docker/claude-sandbox/README.md | 14 ++++++++++++++ docker/claude-sandbox/test/static.bats | 6 ++++++ 3 files changed, 33 insertions(+) diff --git a/bin/claude-sandbox b/bin/claude-sandbox index a25b4a5..abab74a 100755 --- a/bin/claude-sandbox +++ b/bin/claude-sandbox @@ -37,6 +37,19 @@ if [ -n "$SANDBOX_SSH_KEY" ]; then echo "Warning: $SANDBOX_SSH_KEY is readable by group/other (mode $key_perms)." >&2 echo "Recommended: chmod 600 $SANDBOX_SSH_KEY" >&2 fi + # SANDBOX_SSH_KEY is meant to be a dedicated, low-privilege deploy key + # (see README "Headless git push"), not your real identity - it stays + # loaded in an in-container agent for the whole session, unattended. + # Nothing enforces that convention, so at least catch the common case + # of pointing it at a default personal keyfile by mistake. + case "$(basename -- "$SANDBOX_SSH_KEY")" in + id_rsa|id_ecdsa|id_ed25519|id_dsa|id_ecdsa_sk|id_ed25519_sk) + echo "Warning: $SANDBOX_SSH_KEY looks like a default personal SSH key," \ + "not a dedicated one." >&2 + echo "It will be loaded into the sandbox for the whole unattended session -" \ + "see README 'Headless git push'." >&2 + ;; + esac fi have_forwarded_agent=1 diff --git a/docker/claude-sandbox/README.md b/docker/claude-sandbox/README.md index 9a867dd..3240d49 100644 --- a/docker/claude-sandbox/README.md +++ b/docker/claude-sandbox/README.md @@ -440,6 +440,20 @@ one can't happen. push to) other repos on that host you have access to. This doesn't extend to GitHub: port 22 there is blocked outright, so the forwarded agent has no transport to use for it at all (see "GitHub is read-only" above). +- `SANDBOX_SSH_KEY` (see "Headless git push without agent forwarding") is a + second, independent credential-loading path with the same "signs for any + Gitea repo it has access to" shape as the forwarded agent above - the + mitigation is that it's meant to be a dedicated deploy key scoped to only + the repos that actually need push access, not your personal identity. + That scoping is a convention, not something enforced here: nothing checks + what `SANDBOX_SSH_KEY` actually points at, so pointing it at your real + `~/.ssh/id_ed25519`/`id_rsa` instead of a dedicated key would load your + full personal identity into the container for the whole session (`bin/ + claude-sandbox` warns if this looks like it might be happening - see + below - but doesn't block it). The key source itself is mounted read-only + at `/root/...`, not under the sandbox user's own home, specifically so + that user (which the session actually runs as) has no path to read the + raw key material - only the agent socket it's loaded into. - The sandbox's own user (matching your host user — see "Config parity" above) has no `sudo`/setuid path back to root once the container is up: `entrypoint.sh` runs as root, sets up the firewall directly, then drops to diff --git a/docker/claude-sandbox/test/static.bats b/docker/claude-sandbox/test/static.bats index 96c5f08..a25d3d9 100644 --- a/docker/claude-sandbox/test/static.bats +++ b/docker/claude-sandbox/test/static.bats @@ -216,3 +216,9 @@ setup() { grep -q 'SANDBOX_SSH_KEY' "$REPO_ROOT/bin/claude-sandbox" grep -q 'export SANDBOX_SSH_KEY_FILE' "$REPO_ROOT/bin/claude-sandbox" } + +@test "bin/claude-sandbox warns when SANDBOX_SSH_KEY looks like a default personal key" { + grep -q 'looks like a default personal SSH key' "$REPO_ROOT/bin/claude-sandbox" + run grep -E '^\s*id_rsa\|id_ecdsa\|id_ed25519\|id_dsa\|id_ecdsa_sk\|id_ed25519_sk\)\s*$' "$REPO_ROOT/bin/claude-sandbox" + [ "$status" -eq 0 ] +}