
Running Claude Code agents on self-hosted hardware means you own the security perimeter — completely. When I moved my setup to a Mac Mini cluster running Ollama and Draw Things locally, the first thing I noticed was how freely the agent reached out to the internet. npm install, pip install, curl to random CDNs — none of it asked permission. This post walks through the Docker network isolation and iptables whitelist setup I use to lock that down to exactly the traffic I intend.
The Problem: Agents Are Eager, Not Careful
The agent doesn't ask whether it should install a dependency — it just does. This is actually a useful property in a sandboxed environment, but completely the wrong default when the agent has full network access.
Here's what I observed before any isolation. During a typical coding session, the agent contacted an average of 12 external domains per session. These included package registries, CDN hosts for documentation, and the occasional telemetry endpoint from a library. None of this was malicious, but none of it was intentional either.
The failure mode I'm worried about isn't a dramatic breach. It's quieter: an agent pulls a transitive dependency with a known CVE, or exfiltrates a path hint buried in an error message to an external logging endpoint. When you're self-hosting models specifically to keep data local, this defeats the point entirely.
The moment I added iptables logging and watched what the agent was actually doing, the case for isolation closed itself.
The Fix: Docker Internal Network + iptables Whitelist
Step 1: Create an Isolated Bridge Network
Docker's --internal flag is the key primitive here. It creates a bridge network that routes between containers on the same network but has no path to the external internet — the routing table simply doesn't include an external gateway.
# Create the agent-only isolated network
docker network create \
--driver bridge \
--internal \
--subnet 172.30.0.0/24 \
claude-agent-net
# Attach the Ollama container to the same network
# (internal communication only — no internet needed for inference)
docker network connect claude-agent-net ollama
Now run the agent container on this network:
docker run --rm \
--network claude-agent-net \
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
-v $(pwd)/workspace:/workspace \
claude-agent:latest
At this point, the agent can reach Ollama on 172.30.0.x:11434, but has zero external routing. The --internal flag isn't a firewall rule — it's a missing route. There's nothing to block because there's no path out.
Step 2: Punch a Hole for Anthropic API Only
The problem with fully internal networks is that the agent still needs to reach api.anthropic.com to function. For that, I use iptables to carve out a single CIDR allowance while keeping everything else dropped.
# Find the bridge interface Docker created for this network
BRIDGE=$(docker network inspect claude-agent-net \
--format '{{.Options}}' | grep -o 'br-[a-z0-9]*')
# Default policy: drop all forwarding from this bridge to external
iptables -I FORWARD -i $BRIDGE -o eth0 -j DROP
# Whitelist Anthropic API endpoint range (api.anthropic.com resolves here)
iptables -I FORWARD -i $BRIDGE -o eth0 \
-d 18.238.0.0/15 -p tcp --dport 443 -j ACCEPT
Verify the rule order (order matters — iptables processes top-down):
iptables -L FORWARD -n --line-numbers
Expected output:
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
1 ACCEPT tcp -- 0.0.0.0/0 18.238.0.0/15 tcp dpt:443
2 DROP all -- 0.0.0.0/0 0.0.0.0/0
Rule 1 catches Anthropic API traffic first, rule 2 drops everything else. The ACCEPT must come before the DROP — if you add them in the wrong order, the DROP fires first and blocks everything including the API.
Monitoring: Catch What the Agent Tries to Reach
Isolation without visibility is hope, not security. I add a logging rule so every blocked packet gets written to the kernel log.
# Add logging rule BEFORE the DROP rule
iptables -I FORWARD -i $BRIDGE -o eth0 -j LOG \
--log-prefix '[AGENT-BLOCKED] ' --log-level 4
# Stream blocked attempts in real time
journalctl -k -f | grep 'AGENT-BLOCKED'
What this looks like during an active session:
[AGENT-BLOCKED] IN=br-a3f2 OUT=eth0 SRC=172.30.0.3 DST=151.101.1.57
[AGENT-BLOCKED] IN=br-a3f2 OUT=eth0 SRC=172.30.0.3 DST=104.18.32.68
[AGENT-BLOCKED] IN=br-a3f2 OUT=eth0 SRC=172.30.0.3 DST=54.230.19.44
Those destination IPs resolve to Fastly CDN, Cloudflare, and an S3 edge node — all consistent with package manager behavior. The agent was attempting outbound installs that I never asked for, silently, mid-task.
Variations and Gotchas
Making the iptables Rules Persistent
By default, iptables rules disappear on reboot. On Ubuntu/Debian:
apt install iptables-persistent
netfilter-persistent save
On systems using nftables as the default (Debian 12+, newer Ubuntu), translate to nft syntax or explicitly load iptables-legacy. Mixing the two causes silent rule conflicts.
The BRIDGE Variable Can Break on Restart
Docker assigns a new bridge interface name (br-xxxxxxxx) when the network is recreated. If you tear down and recreate claude-agent-net, re-run the BRIDGE= detection and re-apply the iptables rules. I wrap this in a startup script:
#!/bin/bash
BRIDGE=$(docker network inspect claude-agent-net \
--format '{{json .Options}}' | python3 -c \
"import sys,json; opts=json.load(sys.stdin); print(opts.get('com.docker.network.bridge.name',''))")
if [ -z "$BRIDGE" ]; then
BRIDGE=$(ip link | grep -o 'br-[a-z0-9]\{12\}')
fi
iptables -I FORWARD -i $BRIDGE -o eth0 -j LOG \
--log-prefix '[AGENT-BLOCKED] ' --log-level 4
iptables -I FORWARD -i $BRIDGE -o eth0 -j DROP
iptables -I FORWARD -i $BRIDGE -o eth0 \
-d 18.238.0.0/15 -p tcp --dport 443 -j ACCEPT
Anthropic's IP Range Can Shift
The CIDR 18.238.0.0/15 covers the CloudFront distribution Anthropic uses, but this isn't guaranteed stable. A more robust approach is to resolve api.anthropic.com at startup and add the current IPs dynamically, or use an egress proxy like Squid with a hostname allowlist instead of IP-based rules.
Mac / OrbStack Differences
On macOS with OrbStack or Docker Desktop, the bridge interface is abstracted behind a VM layer. The iptables approach won't work directly — the firewall lives inside the Linux VM. Either drop into the OrbStack VM shell to apply rules, or use OrbStack's network policies if available. For my Mac Mini cluster I run a minimal NixOS VM via UTM to host the Docker daemon, which gives full iptables access.
| Environment | iptables accessible? | Alternative |
|---|---|---|
| Linux bare metal | Yes, direct | — |
| Docker Desktop (Mac) | No (VM boundary) | OrbStack network policies |
| OrbStack (Mac) | Inside VM only | orb shell + iptables |
| NixOS VM on Mac | Yes, full control | Best option for self-hosted |
Closing
The combination of --internal Docker network plus targeted iptables rules took about 30 minutes to set up and costs zero milliseconds of added latency on internal Ollama inference — I measured it. External traffic went from 12 domains per session to exactly 1 (Anthropic API). The agent still works identically from a task perspective; it just can't reach anywhere it shouldn't.
If you're self-hosting Claude Code for data locality reasons, the network boundary is where that promise lives or dies. Set the fence before you need it.
Next: I'm looking at adding per-session ephemeral containers so each agent run gets a fresh network namespace and no shared state persists between tasks.
🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Practical
💌 Subscribe: Follow on X or grab the RSS
댓글
댓글 쓰기