Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
34 KiB
Network egress guard for claude-bottle containers
Research into preventing data exfiltration from Docker containers running
Claude Code (--dangerously-skip-permissions), with a focus on approaches
that work on both macOS Docker Desktop and Linux Docker Engine.
Summary
- The container currently has unrestricted internet egress. This is the primary remaining threat surface after the SSH key and credential isolation work done in earlier PRDs.
- The canonical reference implementation is Anthropic's own
.devcontainer/init-firewall.sh, which uses iptables + ipset inside the container to build a default-deny allowlist resolved at launch time. BothRchGrav/claudeboxand theIVIJL/devboxfork adapt the same pattern, the latter adding a dnsmasq layer. - An in-container iptables approach works on macOS Docker Desktop and on
Linux Engine but requires
NET_ADMINandNET_RAWcapabilities, which expand the container's privilege surface by a well-understood, bounded amount. - A proxy sidecar (HTTP CONNECT, no TLS termination) is the cleanest
architecture for a low-trust threat model: the agent process cannot
reconfigure the proxy, and the proxy enforces domain-name semantics
rather than IP addresses that rotate. Stripe's
smokescreenis the most security-hardened option in this space. - DNS exfiltration (encoding data into query subdomains) is real and is not stopped by IP-based allowlists alone. A controlled resolver (dnsmasq or CoreDNS) that answers NXDOMAIN for non-allowlisted names is the complementary control.
- eBPF/Cilium and host-side iptables are impractical on macOS Docker Desktop and should not be pursued for v1.
Threat model
The agent runs as the node user inside the container with
--dangerously-skip-permissions. It can execute arbitrary shell commands,
read any file the user mounts into the container, and make arbitrary network
connections. The host system already isolates it through the container
boundary. The specific threats we are closing in this work are:
- HTTP(S) data exfiltration — agent POST-ing sensitive files or environment variables to an attacker-controlled endpoint on the public internet.
- DNS tunnel exfiltration — encoding data into subdomains of an attacker-controlled zone; the data escapes via DNS queries even when outbound TCP/UDP is blocked.
- Lateral movement to host services — agent reaching RFC 1918 addresses
(host metadata service at 169.254.169.254, host Docker socket via bridge
IP, LAN hosts). The Docker bridge IP is typically
172.17.0.1and is reachable by default. - Arbitrary outbound TCP — the agent opening sockets to unexpected hosts; e.g., a package dependency phoning home to an attacker's C2.
What we are not defending against in this work:
- Kernel-level container escape (a separate, much harder problem).
- A compromised SSH remote host leaking data indirectly through a permitted SSH session (the per-agent allowlist covers this by design).
- Supply-chain attacks in the container image itself (defend via pinned digests and image scanning, not egress rules).
Approach 1: --network none
How it works
docker run --network none removes the container from all networks except
the loopback interface. No default bridge, no host route, no DNS resolver.
Egress decisions expressible
Binary: either the container has no network or it has full network. There is no middle ground without additional plumbing (e.g., connecting the container to a second, controlled network after creation).
DNS handling
With --network none, Docker does not inject a DNS stub resolver. The
container cannot resolve any hostnames.
macOS Docker Desktop compatibility
Fully compatible; --network none is a namespace operation inside the Linux
VM and works identically on both platforms.
Implementation effort
Trivial to add --network none to docker run. Making Claude Code
functional again requires either:
- Connecting the container to a second, tightly controlled Docker network that has a dedicated DNS resolver and outbound proxy, or
- Using
docker network connectafter launch to attach a controlled bridge.
Failure modes / bypasses
Complete loss of connectivity until reconnected. Claude Code will fail at
startup trying to reach api.anthropic.com. This option is not viable as a
standalone solution; it is only the starting point for an allowlist
architecture where the container gets precisely the network it needs.
Protocol coverage
Blocks all IP (TCP, UDP, ICMP) by removing the interface. Also blocks DNS exfiltration — there is no resolver to send queries to.
Agent bypass surface
An agent with shell access cannot add a network interface (no CAP_NET_ADMIN
without explicit grant). Hard to bypass.
Verdict: impractical standalone; useful as a foundation for a proxy-only network architecture where the container's only route is through a sidecar.
Approach 2: iptables inside the container
How it works
Run iptables and ipset inside the container at startup. Set default
OUTPUT policy to DROP, then selectively ACCEPT outbound traffic to a set of
resolved IP addresses. This is the approach taken by Anthropic's own
.devcontainer/init-firewall.sh (source)
and adopted verbatim (or with minor modifications) by
centminmod/claude-code-devcontainers
and IVIJL/devbox.
The script at container startup:
# flush existing rules
iptables -F && iptables -X
iptables -t nat -F && iptables -t nat -X
iptables -t mangle -F && iptables -t mangle -X
ipset destroy allowed-domains 2>/dev/null || true
# default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# unconditional allows
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # DNS
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT # SSH
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# allowlisted domains
ipset create allowed-domains hash:net
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
# populate ipset by DNS resolution at launch
for domain in \
"api.anthropic.com" \
"statsig.anthropic.com" \
"sentry.io" \
"registry.npmjs.org" \
# ... per-agent additions
; do
while read -r ip; do
ipset add allowed-domains "$ip/32" 2>/dev/null || true
done < <(dig +noall +answer A "$domain" | awk '$4=="A"{print $5}')
done
# GitHub IP ranges (dynamic, fetched from GitHub meta API before lockdown)
curl -s https://api.github.com/meta | jq -r '.web[], .api[], .git[]' | \
while read -r cidr; do ipset add allowed-domains "$cidr" 2>/dev/null || true; done
Anthropic's own script also hardcodes their published CIDR (160.79.104.0/23)
to handle the case where api.anthropic.com resolves differently.
RchGrav/claudebox stores the per-project allowlist at
~/.claudebox/<project-name>/firewall/allowlist and exposes a
claudebox allowlist command to view and edit it. The underlying mechanics
are the same iptables + ipset pattern.
IVIJL/devbox adds dnsmasq as an in-container resolver. It seeds
allowed-domains.conf with the domain list, configures dnsmasq to respond
only to those names, and also builds the ipset from the same list. This
provides dual-layer protection: DNS-level blocking and IP-level blocking.
Its CLI (devbox allow <domain>, devbox deny <domain>) reloads dnsmasq and
updates ipset without restarting the container.
Capabilities required
NET_ADMIN and NET_RAW. Without these, iptables and ipset commands
inside the container fail with Operation not permitted.
docker run --cap-add NET_ADMIN --cap-add NET_RAW ...
NET_ADMIN is a meaningful privilege escalation: it allows the process to
modify network interfaces and routing tables, load kernel modules that affect
networking, and so on. However, the scope is limited to the container's
network namespace (not the host's), and this capability is already well
understood in the Docker security literature. The threat model here (misbehaving
LLM agent, not a kernel-exploit attempt) makes this a reasonable trade-off.
Egress decisions expressible
IP address or CIDR. The allowlist is populated at launch by resolving domain
names to IPs; the IPs are what the firewall actually checks. CDNs and cloud
providers that serve many hostnames behind shared IP ranges can cause
over-permissioning (adding api.github.com CIDR also covers all of GitHub
Pages, for example).
DNS handling
Outbound UDP port 53 is explicitly allowed, so the container can reach any resolver. This means DNS exfiltration is not prevented by this approach alone. The IVIJL/devbox variant closes this gap by routing DNS to an in-container dnsmasq instance that only answers for allowlisted names; queries for non-allowlisted names return NXDOMAIN, and the default-deny iptables rule prevents the agent from reaching a third-party resolver directly.
macOS Docker Desktop compatibility
The container iptables approach works on Docker Desktop for macOS. The iptables rules run inside the Linux VM's container namespace, not on the macOS host kernel. This is distinct from approach 2b below (host-side iptables), which does not work on macOS Desktop.
Implementation effort
Moderate. The script itself is well-understood and can be lifted nearly
verbatim from Anthropic's devcontainer repo. The integration points in
cli.sh are:
- Pass
--cap-add NET_ADMIN --cap-add NET_RAWin thedocker runinvocation. docker cpaninit-firewall.shscript into the container (alongside the per-agent allowlist entries derived from the manifest'sssharray and any agent-levelallowlistkey).- Run the script via
docker execbefore handing off to claude.
Failure modes
- Rotating IPs: DNS-resolved IPs are captured once at container startup. If the upstream service rotates IPs (common for CDNs), connections begin failing silently. Periodic re-resolution (a cron inside the container) or using CIDR ranges instead of individual IPs mitigates this.
- Duplicate IPs from DNS: Domains served by large CDNs return many
A records; some resolve to IPs already in the set from a different domain.
Use
ipset add --existto suppress the error (this is the fix for GitHub issue #15611 and #35197). - Agent reconfigures iptables: If the agent can run
sudo iptables -Fit can tear down the firewall. Mitigation: thenodeuser should not have passwordless sudo for iptables after the init script completes. The init script runs as root in an entrypoint; the main claude process runs asnode. Do not grantnodeuser NET_ADMIN or sudo access to iptables commands. - DNS exfil (as noted above): mitigated by adding dnsmasq.
2b: iptables on the Docker host (DOCKER-USER chain)
On Linux Docker Engine, the host kernel runs iptables rules in a DOCKER-USER
chain that is evaluated before Docker's own FORWARD rules. This allows
host-level per-container egress control without giving the container any
elevated capabilities.
# On the Linux host: block all forwarded traffic from a specific container subnet
iptables -I DOCKER-USER -s 172.28.0.0/24 -j DROP
# Then add exceptions per IP
iptables -I DOCKER-USER -s 172.28.0.0/24 -d 160.79.104.0/23 -j RETURN
On macOS Docker Desktop, this is not available from the macOS host.
Docker Desktop runs inside a Linux VM (LinuxKit). The macOS host has no
iptables binary, and there is no supported mechanism to inject rules into
the LinuxKit VM's iptables from the outside. Multiple Docker Desktop
GitHub issues (e.g., #2489,
#5547) document that host
network namespace manipulation does not work as expected on macOS Desktop.
Verdict for 2b: Linux-only. Not viable for this project which must support macOS Docker Desktop.
Approach 3: HTTP(S) egress proxy sidecar
How it works
A second container (or a process in the same container) runs an HTTP forward
proxy. The agent container is configured with HTTPS_PROXY and HTTP_PROXY
pointing at the proxy. The proxy enforces a hostname-based allowlist: it
passes CONNECT requests for allowed hostnames and rejects all others. No TLS
termination is required — the proxy sees only the hostname from the CONNECT
header, not the decrypted payload.
Claude Code fully supports proxy configuration via standard environment variables. From the official network configuration docs:
Claude Code respects standard proxy environment variables:
https_proxy>HTTPS_PROXY>http_proxy>HTTP_PROXY
NODE_EXTRA_CA_CERTS is also supported, enabling trust for a custom CA if
TLS inspection is desired. Note: there are documented bugs where
NODE_EXTRA_CA_CERTS set via settings.json does not always take effect;
setting it as a real environment variable (passed via docker run -e) works
reliably (issue #10458).
Claude Code does not support SOCKS proxies (confirmed in the docs).
Stripe smokescreen
stripe/smokescreen is a
purpose-built HTTP CONNECT proxy designed specifically for outbound egress
control. Key properties:
- Enforces a YAML-format hostname ACL with
open,report, andenforcepolicies. Wildcard hostnames (*.example.com) are supported. - Resolves all requested hostnames and verifies the resulting IPs are public
(not RFC 1918 / link-local / loopback). This blocks lateral movement to
172.17.0.1(Docker bridge),169.254.169.254(metadata service), and private LAN hosts by default — without any additional configuration. - Custom
--allow-range/--deny-rangeCIDR flags for exceptions. - Per-client ACL via mTLS client certificates; not required for the single-agent use case.
- Written in Go; available as a Docker image
(
pretix/smokescreen).
Example ACL YAML:
services:
default:
action: enforce
allowed_hosts:
- api.anthropic.com
- statsig.anthropic.com
- sentry.io
- registry.npmjs.org
- raw.githubusercontent.com
- "*.github.com"
A minimal sidecar setup using Docker Compose (illustrative):
services:
proxy:
image: pretix/smokescreen:latest
command: ["--listen-ip", "0.0.0.0", "--listen-port", "4750", "--config-file", "/config/acl.yaml"]
volumes:
- ./smokescreen-acl.yaml:/config/acl.yaml:ro
networks:
- agent-net
claude-agent:
image: claude-bottle:latest
environment:
HTTPS_PROXY: "http://proxy:4750"
HTTP_PROXY: "http://proxy:4750"
NO_PROXY: "localhost,127.0.0.1"
networks:
- agent-net
cap_drop:
- ALL
In a bash-first project without Docker Compose, the equivalent is:
# create isolated network
docker network create --internal agent-net-"$slug"
# launch proxy sidecar
docker run -d --name "$proxy_name" \
--network agent-net-"$slug" \
-v "$acl_file":/config/acl.yaml:ro \
pretix/smokescreen --config-file /config/acl.yaml
# launch agent, no cap-add needed
docker run -d --name "$container_name" \
--network agent-net-"$slug" \
-e HTTPS_PROXY=http://"$proxy_name":4750 \
-e HTTP_PROXY=http://"$proxy_name":4750 \
claude-bottle:latest
The --internal flag on the network prevents containers from reaching
outside the Docker network by default; the proxy container breaks out via
a second network attachment to the external bridge.
tinyproxy
tinyproxy is a lighter HTTP proxy
(~50 KB) with a simpler allowlist model. It supports
Filter directives using POSIX
extended regular expressions matched against the CONNECT hostname. It does
not resolve DNS or check RFC 1918 ranges — lateral movement to internal IPs
must be blocked separately. For a low-threat-model use case tinyproxy is
adequate; for this project's threat model, smokescreen's automatic RFC 1918
rejection is worth the additional image size.
Egress decisions expressible
Hostname from the CONNECT header (i.e., the domain name the client claims to
be connecting to). Because TLS is not terminated, the proxy cannot verify that
the SNI in the TLS ClientHello matches the CONNECT header. This enables
domain fronting: an agent that knows the proxy ACL could send
CONNECT allowed-host.com:443 but embed a different Host header inside the
TLS session, reaching a different backend on a CDN that routes by SNI. This is
the same limitation acknowledged in Claude Code's own sandboxing docs
(security limitations).
It is a real bypass for sophisticated actors but unlikely to be the first
thing a misbehaving LLM agent attempts.
DNS handling
The proxy resolves hostnames itself before forwarding. The agent container
still needs a working DNS resolver to resolve proxy by name (or the proxy
can be addressed by IP / Docker network alias). To prevent DNS exfiltration,
the container's resolver should be restricted (see Approach 4). DNS queries
to the real internet do not exfiltrate data through a proxy; they exfiltrate
through the resolver.
macOS Docker Desktop compatibility
Fully compatible. Docker bridge networks work identically on both platforms. The proxy sidecar is itself a standard container with no special capabilities.
Implementation effort
Moderate-to-high. New infrastructure (proxy sidecar container, ACL file
format, per-agent ACL generation from the manifest). The manifest already has
an ssh array that lists target SSH hosts; these would seed the agent's ACL
automatically.
Protocol coverage
The proxy only covers HTTP and HTTP CONNECT (HTTPS). It does not cover:
- Raw TCP (e.g., direct database connections, redis, custom protocols).
- UDP.
- ICMP.
- DNS queries.
For raw TCP non-HTTP traffic, combine with iptables approach or --network none + proxy with SOCKS support. (Note: Claude Code itself doesn't support
SOCKS as a client proxy.)
Agent bypass
If the agent can reach the Docker socket or can enumerate Docker networks, it
may be able to attach to a network that bypasses the proxy. Mitigation: do not
mount the Docker socket into the agent container and use --internal Docker
networks. With no Docker socket and no host-network access, a node-user
agent cannot reconfigure network routing.
Approach 4: DNS-based egress control
How it works
Configure the container to use a controlled DNS resolver (e.g., dnsmasq, CoreDNS, or unbound) that only responds with valid answers for allowlisted hostnames. All other queries receive NXDOMAIN. Pair this with an IP-based default-deny firewall so the agent cannot bypass DNS by hardcoding IP addresses.
dnsmasq example (/etc/dnsmasq.conf for the resolver container):
# deny all by default
address=/#/0.0.0.0
# allowlist
server=/api.anthropic.com/8.8.8.8
server=/statsig.anthropic.com/8.8.8.8
server=/registry.npmjs.org/8.8.8.8
Then in the agent container: --dns <resolver-ip> and iptables to block
outbound UDP port 53 to anything other than the resolver.
DNS exfiltration
This approach does not prevent DNS exfiltration if the agent can send queries
to the allowlisted resolver with attacker-crafted subdomains. A resolver that
forwards queries for api.anthropic.com will also forward queries for
data.api.anthropic.com if the upstream (8.8.8.8) resolves them. Genuine
prevention of DNS exfiltration requires either:
- A resolver that blocks queries with subdomains deeper than expected for each allowlisted domain (fragile and maintenance-heavy), or
- A custom authoritative resolver for allowlisted names only, with no
forwarding for unknown subdomains. dnsmasq with
local=/anthropic.com/(authoritative, no recursion) can approximate this but breaks legitimate multi-level lookups.
In practice, DNS exfiltration through an allowlisted domain requires the attacker to control DNS for that domain, which is a significant bar. It is a real threat for high-value targets but is secondary to the baseline of blocking arbitrary HTTP(S) egress.
macOS Docker Desktop compatibility
A dnsmasq or CoreDNS container running on the same Docker network is fully
compatible with macOS Docker Desktop. The --dns flag on docker run works
on both platforms.
Implementation effort
Low-to-moderate as a complement to approach 2 or 3, not as a standalone control. The IVIJL/devbox project demonstrates the full stack (iptables + ipset + dnsmasq) in bash.
Protocol coverage
Blocks name resolution for non-allowlisted domains. Does not directly block IP-addressed connections; must be paired with IP-level controls.
Approach 5: eBPF / Cilium
How it works
Cilium uses eBPF programs attached to network interfaces inside the kernel to enforce network policy at L3–L7, including DNS-aware hostname policies. It is the most powerful option architecturally.
macOS Docker Desktop compatibility
Not practical. eBPF requires BTF (BPF Type Format) kernel support. Docker
Desktop's LinuxKit VM on macOS does not ship kernel headers and has limited
BTF support; recent versions (Docker Desktop 4.x with kernel 6.4+) have basic
BTF but Cilium requires specific kernel configuration options that LinuxKit
does not set (Docker for Mac issue #6800).
Cilium's own documentation requires a Kubernetes cluster; its standalone
Docker plugin (cilium/docker-plugin) is not maintained to parity.
Verdict
Out of scope for this project. Revisit if the project ever targets a Kubernetes-native deployment.
Approach 6: Userspace proxy embedded in the agent image
How it works
Ship a lightweight proxy binary (e.g., smokescreen or a small Go binary)
inside the agent image itself. The entrypoint starts the proxy on localhost,
then exports HTTPS_PROXY=http://127.0.0.1:4750 before launching claude.
Pros vs. sidecar
- Single container; simpler orchestration.
- No Docker network management.
Cons vs. sidecar
- The proxy runs as a process inside the same container. The agent (running
as
node) could potentially kill the proxy process or modify its config if it has enough shell access. A sidecar in a separate container cannot be killed by the agent regardless of what the agent does. - Harder to update the ACL independently of the agent image.
- The proxy binary adds size to the agent image.
Verdict
Acceptable for a first iteration where simplicity matters more than strict isolation of the enforcement point. A sidecar is preferred once the architecture stabilizes.
Approach 7: mitmproxy in transparent mode (L7 inspection)
How it works
mitmproxy in transparent mode intercepts TLS by terminating the connection,
re-encrypting with a CA it controls, and forwarding. This enables full URL
inspection (not just the CONNECT hostname). The agent must trust the proxy's
CA, injected via NODE_EXTRA_CA_CERTS.
For this to work:
- The agent image must have the mitmproxy CA cert installed and
NODE_EXTRA_CA_CERTSset. - iptables inside the container (or on the Docker network) must redirect all port 443 traffic to the proxy via NAT.
- mitmproxy runs an addon script (
addons.py) that checks the request URL against the allowlist and returns a 403 for blocked URLs.
Example:
# mitmproxy addon
class AllowlistAddon:
ALLOWED = {"api.anthropic.com", "registry.npmjs.org"}
def request(self, flow):
if flow.request.host not in self.ALLOWED:
flow.response = mitmproxy.http.Response.make(
403, b"Blocked by egress policy")
Claude Code TLS handling
Claude Code uses Node.js and respects NODE_EXTRA_CA_CERTS when set as an
environment variable (not via settings.json, which has a documented bug).
The CLAUDE_CODE_CERT_STORE variable controls whether it uses the bundled
Mozilla CA set and/or the OS trust store. Setting CLAUDE_CODE_CERT_STORE=system
and injecting the mitmproxy CA into the container's system trust store is the
most reliable path.
There is a separate known issue with the Bun-native build of Claude Code
(@anthropic-ai/claude-code 1.x uses a native Bun binary on some platforms)
where Bun does not read macOS system certificates. Inside a Linux container
this is not relevant — the binary uses the Linux certificate store.
Concerns
- CA trust injection is a meaningful security assumption: if the CA key is ever leaked, all HTTPS from the container is transparent to the attacker. The CA key should be generated fresh per-agent-launch and discarded after.
- Transparent mode requires NAT, which requires
NET_ADMINinside the container (same capability requirement as approach 2). - Operational complexity is significantly higher than approaches 2 or 3.
- Domain fronting is defeated by TLS inspection (the proxy can compare the SNI with the HTTP Host header), but this is overkill for the current threat model.
Verdict
Justified only if the threat model includes sophisticated actors deliberately crafting domain-fronting payloads. The extra complexity and CA-trust-management overhead is not worth it for v1. Keep in view for v2 if the claude-bottle use case expands to high-value agent deployments.
Comparison table
| Approach | Granularity | DNS-safe | macOS-friendly | Impl. effort | Bypass surface |
|---|---|---|---|---|---|
1. --network none + re-add |
On/Off (network-level) | Yes (no resolver) | Yes | Low (standalone); High (functional) | Very low |
| 2a. iptables in container | IP / CIDR | No (unless + dnsmasq) | Yes | Low–Medium | Needs NET_ADMIN; agent can't undo without sudo |
| 2b. iptables on host (DOCKER-USER) | IP / CIDR | No | No | Medium | macOS VM boundary |
| 3. HTTP proxy sidecar (smokescreen) | Hostname (CONNECT) | Partial (no DNS exfil guard) | Yes | Medium | Domain fronting; agent can't kill sidecar |
| 4. DNS resolver (dnsmasq/CoreDNS) | DNS name only | Partial (no IP bypass guard) | Yes | Low (complement) | Hardcoded IPs bypass DNS; DNS sub-exfil |
| 5. eBPF / Cilium | L3–L7 | Yes | No | Very High | None in practice |
| 6. Proxy embedded in image | Hostname (CONNECT) | Partial | Yes | Medium | Agent can kill proxy process |
| 7. mitmproxy transparent | Full URL | Yes | Yes (complex NAT) | High | CA key leakage; NAT escape |
Anthropic's official guidance
Anthropic maintains an init-firewall.sh
in the claude-code devcontainer that implements approach 2a. The
official sandboxing documentation
describes the @anthropic-ai/sandbox-runtime package which uses a
host-side HTTP + SOCKS5 proxy with OS-level namespace isolation (bubblewrap
on Linux, Seatbelt on macOS) — effectively approach 3 implemented at the
OS level rather than the Docker level.
The required runtime hostnames from the network config docs are:
| Hostname | Purpose |
|---|---|
api.anthropic.com |
Claude API requests |
claude.ai |
claude.ai account auth |
platform.claude.com |
Anthropic Console auth |
downloads.claude.ai |
Plugin / native installer downloads |
raw.githubusercontent.com |
Changelog feed |
statsig.anthropic.com |
Telemetry (can be disabled) |
sentry.io |
Error telemetry (can be disabled) |
Telemetry can be disabled via environment variables; doing so shrinks the allowlist and reduces exfiltration surface at the cost of losing Anthropic's error reporting.
Claudebox implementation (RchGrav)
RchGrav/claudebox stores per-project firewall configuration in
~/.claudebox/<project-name>/firewall/allowlist and exposes a
claudebox allowlist command to view and edit it. The underlying
implementation mirrors Anthropic's init-firewall.sh (iptables + ipset,
approach 2a). The script flushes iptables rules and rebuilds the ipset at
container startup by resolving the domains in the allowlist file. GitHub IP
ranges are fetched dynamically from api.github.com/meta. The README does
not expose the precise file format of allowlist; from the README content it
appears to be a newline-delimited list of hostnames.
A closer implementation reference is the devbox fork by IVIJL
(IVIJL/devbox) which adds dnsmasq on
top of the same iptables + ipset skeleton and provides a live-update CLI
(devbox allow, devbox deny, devbox blocked) that reloads dnsmasq and
updates ipset without a container restart.
Recommendation
Tier 1 (v1, implement first): in-container iptables + ipset + dnsmasq
Adopt approach 2a with the dnsmasq complement from IVIJL/devbox. This is the
pattern validated by Anthropic's own devcontainer, is bash-first, adds no new
runtime dependencies (iptables and ipset are standard in the base Debian/Ubuntu
image used by Claude Code; dnsmasq is a single apt-get install), and works
on both macOS Docker Desktop and Linux Docker Engine.
Key PRD scope for this work:
- Add
--cap-add NET_ADMIN --cap-add NET_RAWto thedocker runinvocation inlib/(container launch). - Add
init-firewall.shas a templated script that the launch processdocker cps into the container, parameterized with:- The base allowlist (Anthropic API, npm, telemetry).
- Per-agent additions from a new optional
allowlistkey in the manifest (newline-separated hostnames, same format as thessharray logic). - The SSH target hostnames from the existing
ssharray (already in the manifest), automatically seeded into the firewall allowlist.
- Run the script via
docker exec --user rootbefore handing off to claude. - Add a dnsmasq configuration step: install dnsmasq in the Dockerfile, configure
it to serve only allowlisted names, and set
--dns 127.0.0.1indocker run. - Block outbound UDP port 53 to non-dnsmasq resolvers in the iptables rules.
- Document the
--cap-addchanges inCLAUDE.mdunder security.
Tier 2 (v2, higher isolation): smokescreen sidecar
If the threat model requires that the agent cannot defeat the firewall by
exploiting the NET_ADMIN capability or by running sudo iptables -F (if
sudo is ever accidentally granted), replace or complement the in-container
approach with a smokescreen sidecar. The sidecar runs with no elevated
capabilities, lives in a separate container the agent cannot reach, and enforces
RFC 1918 blocking automatically.
Key PRD scope additions over Tier 1:
- New
proxysection in the manifest (or auto-generated per-agent) describing the smokescreen ACL. - Launch smokescreen container on a
--internalDocker network before the agent container. - Attach the agent container to the internal network only; smokescreen has a second NIC on the external bridge.
- Inject
HTTPS_PROXYandHTTP_PROXYinto the agent container. - Generate a per-launch smokescreen ACL YAML from the manifest's
allowlistandsshentries.
Deferred / out of scope
- Layer 7 TLS inspection (approach 7): CA management overhead and domain fronting defence are out of scope for v1. Revisit if the project adds multi-user or high-value deployment scenarios.
- eBPF / Cilium (approach 5): requires Kubernetes and does not work on macOS Docker Desktop without non-trivial kernel configuration.
- Host-side iptables (approach 2b): Linux-only; excluded because the project must work on macOS Docker Desktop.
- SOCKS proxy: Claude Code does not support SOCKS as a client proxy (confirmed in official docs).
- Full DNS exfiltration prevention: preventing data encoding into DNS sub-labels of an allowlisted domain requires authoritative DNS for those domains and is not achievable in a general-purpose egress guard.
- Container image signing / supply chain: out of scope; use pinned digests and image scanning separately.
References
- Anthropic
init-firewall.sh(devcontainer): https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh - Claude Code network configuration docs: https://code.claude.com/docs/en/network-config
- Claude Code sandboxing docs: https://code.claude.com/docs/en/sandboxing
- Anthropic blog — Claude Code sandboxing: https://www.anthropic.com/engineering/claude-code-sandboxing
anthropic-experimental/sandbox-runtime: https://github.com/anthropic-experimental/sandbox-runtimeRchGrav/claudebox: https://github.com/RchGrav/claudeboxIVIJL/devbox: https://github.com/IVIJL/devboxcentminmod/claude-code-devcontainersinit-firewall: https://github.com/centminmod/claude-code-devcontainers/blob/master/.devcontainer/init-firewall.shstripe/smokescreen: https://github.com/stripe/smokescreenpretix/smokescreenDocker image: https://hub.docker.com/r/pretix/smokescreen- Fly.io smokescreen guide: https://fly.io/docs/app-guides/smokescreen/
- mitmproxy proxy modes: https://docs.mitmproxy.org/stable/concepts/modes/
- Claude Code issue —
NODE_EXTRA_CA_CERTSin settings.json not effective: https://github.com/anthropics/claude-code/issues/10458 - Claude Code issue — init-firewall.sh duplicate IP failure: https://github.com/anthropics/claude-code/issues/35197
- Docker iptables / DOCKER-USER chain docs: https://docs.docker.com/engine/network/firewall-iptables/
- Docker for Mac iptables issue #2489: https://github.com/docker/for-mac/issues/2489
- Docker for Mac BTF / eBPF issue #6800: https://github.com/docker/for-mac/issues/6800
Research conducted 2026-05-07.