Files
bot-bottle/docs/research/network-egress-guard.md
T
didericis cc5e772519
test / run tests/run_tests.py (push) Successful in 13s
docs: replace stale .sh paths with claude_bottle/*.py equivalents
Cleans up references to the pre-refactor bash layout (cli.sh,
lib/*.sh, scripts/*.sh) across README, Dockerfile, the pipelock PRD,
and research notes. Refreshes line numbers in the oauth-token note
against the current cli/start.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:27:25 -04:00

34 KiB
Raw Blame History

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. Both RchGrav/claudebox and the IVIJL/devbox fork 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_ADMIN and NET_RAW capabilities, 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 smokescreen is 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:

  1. HTTP(S) data exfiltration — agent POST-ing sensitive files or environment variables to an attacker-controlled endpoint on the public internet.
  2. 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.
  3. 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.1 and is reachable by default.
  4. 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 connect after 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.py are:

  1. Pass --cap-add NET_ADMIN --cap-add NET_RAW in the docker run invocation.
  2. docker cp an init-firewall.sh script into the container (alongside the per-agent allowlist entries derived from the manifest's ssh array and any agent-level allowlist key).
  3. Run the script via docker exec before 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 --exist to suppress the error (this is the fix for GitHub issue #15611 and #35197).
  • Agent reconfigures iptables: If the agent can run sudo iptables -F it can tear down the firewall. Mitigation: the node user 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 as node. Do not grant node user 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, and enforce policies. 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-range CIDR 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:

  1. A resolver that blocks queries with subdomains deeper than expected for each allowlisted domain (fragile and maintenance-heavy), or
  2. 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 L3L7, 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:

  1. The agent image must have the mitmproxy CA cert installed and NODE_EXTRA_CA_CERTS set.
  2. iptables inside the container (or on the Docker network) must redirect all port 443 traffic to the proxy via NAT.
  3. 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_ADMIN inside 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 LowMedium 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 L3L7 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_RAW to the docker run invocation in lib/ (container launch).
  • Add init-firewall.sh as a templated script that the launch process docker cps into the container, parameterized with:
    • The base allowlist (Anthropic API, npm, telemetry).
    • Per-agent additions from a new optional allowlist key in the manifest (newline-separated hostnames, same format as the ssh array logic).
    • The SSH target hostnames from the existing ssh array (already in the manifest), automatically seeded into the firewall allowlist.
  • Run the script via docker exec --user root before 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.1 in docker run.
  • Block outbound UDP port 53 to non-dnsmasq resolvers in the iptables rules.
  • Document the --cap-add changes in CLAUDE.md under 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 proxy section in the manifest (or auto-generated per-agent) describing the smokescreen ACL.
  • Launch smokescreen container on a --internal Docker 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_PROXY and HTTP_PROXY into the agent container.
  • Generate a per-launch smokescreen ACL YAML from the manifest's allowlist and ssh entries.

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

Research conducted 2026-05-07.