Add quick allowlist for domains in sandbox
This commit is contained in:
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# Add a domain to the egress firewall allowlist of a running claude-sandbox
|
||||
# container, from outside it. The sandbox itself has no privilege to do this
|
||||
# (node has no sudo/setuid path back to root - see docker/claude-sandbox's
|
||||
# security notes); if a sandboxed agent hits a blocked domain it actually
|
||||
# needs, it should tell you the exact command to run here rather than try
|
||||
# to work around the firewall.
|
||||
#
|
||||
# Usage: claude-sandbox-allow <domain> [--global] [--container <name>]
|
||||
# claude-sandbox-allow crates.io # this running session only
|
||||
# claude-sandbox-allow crates.io --global # + persist for future sessions
|
||||
set -euo pipefail
|
||||
|
||||
domain=""
|
||||
global=0
|
||||
container=""
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--global) global=1; shift ;;
|
||||
--container) container="$2"; shift 2 ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*)
|
||||
if [ -n "$domain" ]; then
|
||||
echo "Usage: claude-sandbox-allow <domain> [--global] [--container <name>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
domain="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
echo "Usage: claude-sandbox-allow <domain> [--global] [--container <name>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$container" ]; then
|
||||
containers=$(docker ps --filter "label=com.docker.compose.project=claude-sandbox" --filter "status=running" --format '{{.Names}}')
|
||||
count=$(echo "$containers" | grep -c . || true)
|
||||
if [ "$count" -eq 0 ]; then
|
||||
echo "No running claude-sandbox container found." >&2
|
||||
exit 1
|
||||
elif [ "$count" -gt 1 ]; then
|
||||
echo "Multiple running claude-sandbox containers found; specify one with --container:" >&2
|
||||
echo "$containers" >&2
|
||||
exit 1
|
||||
fi
|
||||
container="$containers"
|
||||
fi
|
||||
|
||||
echo "Resolving $domain inside $container..."
|
||||
ips=$(docker exec "$container" dig +short A "$domain" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' || true)
|
||||
if [ -z "$ips" ]; then
|
||||
echo "Failed to resolve $domain inside the container." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for ip in $ips; do
|
||||
echo "Allowing $ip ($domain)"
|
||||
docker exec -u root "$container" ipset add allowed-domains "$ip" -exist
|
||||
done
|
||||
|
||||
echo "$domain allowed for the running session ($container)."
|
||||
|
||||
if [ "$global" -eq 1 ]; then
|
||||
profile_file="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/config/profile"
|
||||
if [ -f "$profile_file" ] && grep -q '^export ALLOWED_DOMAINS=' "$profile_file"; then
|
||||
current=$(sed -nE 's/^export ALLOWED_DOMAINS="(.*)"$/\1/p' "$profile_file")
|
||||
if [[ " $current " == *" $domain "* ]]; then
|
||||
echo "$domain already in the global ALLOWED_DOMAINS (config/profile)."
|
||||
else
|
||||
new="${current:+$current }$domain"
|
||||
sed -i "s|^export ALLOWED_DOMAINS=.*|export ALLOWED_DOMAINS=\"$new\"|" "$profile_file"
|
||||
echo "Added $domain to config/profile's ALLOWED_DOMAINS (source your shell, or open a new one, for future sessions to pick it up)."
|
||||
fi
|
||||
else
|
||||
echo "Couldn't find 'export ALLOWED_DOMAINS=' in $profile_file to update." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -21,18 +21,33 @@ What this means:
|
||||
disposable: installed system packages, anything outside the mounted
|
||||
paths, and the container itself go away when this run ends.
|
||||
- Outbound network is restricted to an allowlist (Anthropic's API, the npm
|
||||
registry, GitHub, the configured Gitea host, plus anything set via
|
||||
ALLOWED_DOMAINS). Requests to anything else will simply fail to connect.
|
||||
registry, GitHub read-only, the configured Gitea host, plus anything set
|
||||
via ALLOWED_DOMAINS). Requests to anything else will simply fail to
|
||||
connect. If a task needs one more domain and nothing else, that's a quick
|
||||
fix, not a reason to stop: tell the user to run
|
||||
\`bin/claude-sandbox-allow <domain>\` (add \`--global\` to persist it past
|
||||
this session) from a terminal outside the sandbox, then continue.
|
||||
- Stay within the task you were given. Since there are no permission
|
||||
prompts here, be more conservative than usual about anything destructive,
|
||||
irreversible, or outside the project directory.
|
||||
|
||||
If a task genuinely needs something the sandbox can't provide - broader
|
||||
network access, host-level or system changes, GUI/hardware access, or
|
||||
anything blocked by the firewall - stop and explain clearly to the user what
|
||||
you need and why, instead of trying to work around the firewall or sandbox
|
||||
yourself. They can continue this exact session outside the container by
|
||||
running on the host:
|
||||
Git workflow - do this even though nothing stops you from doing otherwise:
|
||||
- Do new work in a git worktree for the feature branch (e.g. \`git worktree
|
||||
add ../<branch> -b <branch>\`), not by switching branches in place in the
|
||||
mounted checkout - that's the user's actual working copy, and changing
|
||||
its branch out from under them is disruptive even if you switch it back.
|
||||
- Push that branch to the Gitea remote, then run \`gitea-pr create\` from
|
||||
the worktree. Never push to GitHub (read-only, and not reachable for
|
||||
push regardless), and never push directly to main/master, on any remote.
|
||||
A \`git-push-guard.sh\` hook enforces both as a backstop, but treat them
|
||||
as the actual workflow to follow, not just a filter to route around.
|
||||
|
||||
If a task genuinely needs something else the sandbox can't provide -
|
||||
broader network access than a domain or two, host-level or system changes,
|
||||
GUI/hardware access - stop and explain clearly to the user what you need
|
||||
and why, instead of trying to work around the firewall or sandbox yourself.
|
||||
They can continue this exact session outside the container by running on
|
||||
the host:
|
||||
|
||||
claude --resume $resume_id
|
||||
|
||||
|
||||
@@ -95,6 +95,12 @@ the firewall backs this up with real enforcement (see below); for "never
|
||||
pushes to main/master", the authoritative version of that is branch
|
||||
protection on the Gitea repo itself, which this doesn't set up for you.
|
||||
|
||||
sandbox-context.sh also tells the agent the actual workflow to follow
|
||||
proactively, not just what's blocked: do new work in a `git worktree`
|
||||
rather than switching branches in the mounted checkout in place (that's the
|
||||
user's real working copy), then push the feature branch and run
|
||||
`gitea-pr create` from the worktree.
|
||||
|
||||
## Network egress firewall
|
||||
|
||||
On start, the container runs an allowlist-only `iptables`/`ipset` firewall
|
||||
@@ -118,6 +124,29 @@ If your Gitea host is only resolvable via a LAN-local DNS server, make sure
|
||||
the Docker host itself can resolve it — containers inherit the host's
|
||||
resolver by default.
|
||||
|
||||
### Adding a domain to a running session
|
||||
|
||||
`ALLOWED_DOMAINS` only takes effect at container start. For a domain a
|
||||
*running* session needs right now, run from the host (not inside the
|
||||
sandbox — it has no privilege to do this itself):
|
||||
|
||||
```sh
|
||||
bin/claude-sandbox-allow crates.io # this session only
|
||||
bin/claude-sandbox-allow crates.io --global # + persist in config/profile
|
||||
```
|
||||
|
||||
It resolves the domain inside the target container (so it sees what the
|
||||
container's own resolver sees) and adds the IP(s) directly to the running
|
||||
firewall's `ipset`, via `docker exec -u root`. `--global` also appends the
|
||||
domain to `ALLOWED_DOMAINS` in `config/profile` for future sessions; source
|
||||
your shell (or open a new one) for that part to take effect. If more than
|
||||
one claude-sandbox container is running, pass `--container <name>` to
|
||||
disambiguate (`docker ps` to find it).
|
||||
|
||||
The sandbox-context.sh `SessionStart` hook tells the agent to ask for this
|
||||
by name when a task is blocked on a single missing domain, rather than
|
||||
stopping the whole session over it.
|
||||
|
||||
## GitHub is read-only
|
||||
|
||||
The sandbox can read from GitHub (clone, fetch, `gh` API calls) but can
|
||||
@@ -192,4 +221,6 @@ GitHub itself (403), not just blocked locally.
|
||||
|
||||
`bin/claude-sandbox` (repo root `bin/`) is the entry point — it resolves the
|
||||
project path, checks for a live `ssh-agent`, loads `GITEA_TOKEN`/`GH_TOKEN`
|
||||
if not already set, and runs `docker compose run`.
|
||||
if not already set, and runs `docker compose run`. `bin/claude-sandbox-allow`
|
||||
adds a domain to a running session's firewall from the host - see "Adding a
|
||||
domain to a running session" above.
|
||||
|
||||
Reference in New Issue
Block a user