Skip to content

Commit 12683ac

Browse files
Mossakaclaude
andcommitted
fix: restrict host gateway iptables bypass to allowed ports only
The --enable-host-access flag added an iptables ACCEPT rule for host.docker.internal with no port restriction, allowing agent code to reach ANY service on the host (databases, admin panels, etc.) and bypassing the dangerous-ports blocklist entirely. Changes: - Restrict host gateway FILTER ACCEPT to ports 80, 443, and any ports from --allow-host-ports (was: all ports) - Apply same port restriction to network gateway bypass - Add IPv4 format validation for dynamically resolved IPs before using them in iptables rules - Mount chroot-hosts as read-only (:ro) since host.docker.internal is pre-injected by docker-manager.ts before mounting The NAT RETURN rule (which prevents DNAT to Squid) is unchanged, so MCP traffic still bypasses Squid correctly. Non-allowed port traffic hits the final DROP rule in the FILTER chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 61432e3 commit 12683ac

3 files changed

Lines changed: 39 additions & 8 deletions

File tree

containers/agent/setup-iptables.sh

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ is_ipv6() {
1111
[[ "$ip" == *:* ]]
1212
}
1313

14+
# Function to validate an IPv4 address format (e.g., 172.17.0.1)
15+
is_valid_ipv4() {
16+
local ip="$1"
17+
echo "$ip" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
18+
}
19+
1420
# Function to check if ip6tables is available and functional
1521
has_ip6tables() {
1622
if command -v ip6tables &>/dev/null && ip6tables -L -n &>/dev/null; then
@@ -124,12 +130,28 @@ iptables -t nat -A OUTPUT -d "$SQUID_IP" -j RETURN
124130
# Bypass Squid for host.docker.internal when host access is enabled.
125131
# MCP gateway traffic to host.docker.internal gets DNAT'd to Squid,
126132
# where Squid fails with "Invalid URL" because rmcp sends relative URLs.
133+
# The NAT RETURN prevents DNAT to Squid (which would crash on MCP traffic).
134+
# The FILTER ACCEPT is restricted to allowed ports only (80, 443, and --allow-host-ports).
127135
if [ -n "$AWF_ENABLE_HOST_ACCESS" ]; then
128136
HOST_GATEWAY_IP=$(getent hosts host.docker.internal | awk 'NR==1 { print $1 }')
129-
if [ -n "$HOST_GATEWAY_IP" ]; then
137+
if [ -n "$HOST_GATEWAY_IP" ] && is_valid_ipv4 "$HOST_GATEWAY_IP"; then
130138
echo "[iptables] Allow direct traffic to host gateway (${HOST_GATEWAY_IP}) - bypassing Squid..."
139+
# NAT: skip DNAT to Squid for all traffic to host gateway (prevents Squid crash)
131140
iptables -t nat -A OUTPUT -d "$HOST_GATEWAY_IP" -j RETURN
132-
iptables -A OUTPUT -d "$HOST_GATEWAY_IP" -j ACCEPT
141+
# FILTER: only allow standard ports (80, 443) to host gateway
142+
iptables -A OUTPUT -p tcp -d "$HOST_GATEWAY_IP" --dport 80 -j ACCEPT
143+
iptables -A OUTPUT -p tcp -d "$HOST_GATEWAY_IP" --dport 443 -j ACCEPT
144+
# FILTER: also allow user-specified ports from --allow-host-ports
145+
if [ -n "$AWF_ALLOW_HOST_PORTS" ]; then
146+
IFS=',' read -ra HOST_PORTS <<< "$AWF_ALLOW_HOST_PORTS"
147+
for port_spec in "${HOST_PORTS[@]}"; do
148+
port_spec=$(echo "$port_spec" | xargs)
149+
echo "[iptables] Allow host gateway port $port_spec"
150+
iptables -A OUTPUT -p tcp -d "$HOST_GATEWAY_IP" --dport "$port_spec" -j ACCEPT
151+
done
152+
fi
153+
elif [ -n "$HOST_GATEWAY_IP" ]; then
154+
echo "[iptables] WARNING: host.docker.internal resolved to invalid IP '${HOST_GATEWAY_IP}', skipping host gateway bypass"
133155
else
134156
echo "[iptables] WARNING: host.docker.internal could not be resolved, skipping host gateway bypass"
135157
fi
@@ -139,10 +161,20 @@ if [ -n "$AWF_ENABLE_HOST_ACCESS" ]; then
139161
# instead of the Docker bridge gateway (172.17.0.1). Without this bypass,
140162
# MCP Streamable HTTP traffic goes through Squid, which crashes on SSE connections.
141163
NETWORK_GATEWAY_IP=$(route -n | awk '/^0\.0\.0\.0/ { print $2; exit }')
142-
if [ -n "$NETWORK_GATEWAY_IP" ] && [ "$NETWORK_GATEWAY_IP" != "$HOST_GATEWAY_IP" ]; then
164+
if [ -n "$NETWORK_GATEWAY_IP" ] && is_valid_ipv4 "$NETWORK_GATEWAY_IP" && [ "$NETWORK_GATEWAY_IP" != "$HOST_GATEWAY_IP" ]; then
143165
echo "[iptables] Allow direct traffic to network gateway (${NETWORK_GATEWAY_IP}) - bypassing Squid..."
144166
iptables -t nat -A OUTPUT -d "$NETWORK_GATEWAY_IP" -j RETURN
145167
iptables -A OUTPUT -p tcp -d "$NETWORK_GATEWAY_IP" --dport 80 -j ACCEPT
168+
iptables -A OUTPUT -p tcp -d "$NETWORK_GATEWAY_IP" --dport 443 -j ACCEPT
169+
if [ -n "$AWF_ALLOW_HOST_PORTS" ]; then
170+
IFS=',' read -ra NET_GW_PORTS <<< "$AWF_ALLOW_HOST_PORTS"
171+
for port_spec in "${NET_GW_PORTS[@]}"; do
172+
port_spec=$(echo "$port_spec" | xargs)
173+
iptables -A OUTPUT -p tcp -d "$NETWORK_GATEWAY_IP" --dport "$port_spec" -j ACCEPT
174+
done
175+
fi
176+
elif [ -n "$NETWORK_GATEWAY_IP" ] && ! is_valid_ipv4 "$NETWORK_GATEWAY_IP"; then
177+
echo "[iptables] WARNING: network gateway resolved to invalid IP '${NETWORK_GATEWAY_IP}', skipping"
146178
fi
147179
fi
148180

src/docker-manager.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ describe('docker-manager', () => {
774774
expect(volumes).toContain('/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro');
775775
});
776776

777-
it('should mount writable chroot-hosts when enableChroot and enableHostAccess are true', () => {
777+
it('should mount read-only chroot-hosts when enableChroot and enableHostAccess are true', () => {
778778
// Ensure workDir exists for chroot-hosts file creation
779779
fs.mkdirSync(mockConfig.workDir, { recursive: true });
780780
try {
@@ -787,11 +787,10 @@ describe('docker-manager', () => {
787787
const agent = result.services.agent;
788788
const volumes = agent.volumes as string[];
789789

790-
// Should mount a writable copy of /etc/hosts (not the read-only original)
790+
// Should mount a read-only copy of /etc/hosts with host.docker.internal pre-injected
791791
const hostsVolume = volumes.find((v: string) => v.includes('/host/etc/hosts'));
792792
expect(hostsVolume).toBeDefined();
793-
expect(hostsVolume).toContain('chroot-hosts:/host/etc/hosts');
794-
expect(hostsVolume).not.toContain(':ro');
793+
expect(hostsVolume).toContain('chroot-hosts:/host/etc/hosts:ro');
795794
} finally {
796795
fs.rmSync(mockConfig.workDir, { recursive: true, force: true });
797796
}

src/docker-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ export function generateDockerCompose(
521521
logger.debug(`Could not resolve Docker bridge gateway: ${err}`);
522522
}
523523
fs.chmodSync(chrootHostsPath, 0o644);
524-
agentVolumes.push(`${chrootHostsPath}:/host/etc/hosts`);
524+
agentVolumes.push(`${chrootHostsPath}:/host/etc/hosts:ro`);
525525
} else {
526526
agentVolumes.push('/etc/hosts:/host/etc/hosts:ro');
527527
}

0 commit comments

Comments
 (0)