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).
This commit is contained in:
2026-07-22 13:03:30 -07:00
parent 5fef8dcf90
commit 621bc28f7a
3 changed files with 33 additions and 0 deletions
+13
View File
@@ -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
+14
View File
@@ -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
+6
View File
@@ -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 ]
}