Skip to content

Commit 7c06edb

Browse files
authored
Merge pull request #32 from stranma/feat/webfetch-firewall-integration
feat: auto-whitelist WebFetch domains in devcontainer firewall
2 parents d6d9d88 + ad0cc69 commit 7c06edb

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

.devcontainer/init-firewall.sh

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ set -euo pipefail
33
IFS=$'\n\t'
44

55
# Network security firewall for devcontainer.
6-
# Restricts egress to: PyPI, GitHub, Anthropic/Claude, VS Code, uv/Astral.
6+
# Restricts egress to: PyPI, GitHub, Anthropic/Claude, VS Code, uv/Astral,
7+
# plus any domains from WebFetch(domain:...) permission patterns.
78
# Uses ipset with aggregated CIDR ranges for reliable filtering.
89

910
echo "iptables version: $(iptables --version)"
@@ -109,6 +110,50 @@ for domain in \
109110
done < <(echo "$ips")
110111
done
111112

113+
# --- Extract domains from WebFetch permission settings ---
114+
extract_webfetch_domains() {
115+
local file="$1"
116+
[ -f "$file" ] || return 0
117+
jq -r '
118+
[(.permissions.allow // []), (.permissions.ask // [])] | add
119+
| .[]
120+
| select(startswith("WebFetch(domain:"))
121+
| sub("^WebFetch\\(domain:"; "") | sub("\\)$"; "")
122+
' "$file" 2>/dev/null || true
123+
}
124+
125+
SETTINGS_DIR="/workspace/.claude"
126+
WEBFETCH_DOMAINS=""
127+
for settings_file in "$SETTINGS_DIR/settings.json" "$SETTINGS_DIR/settings.local.json"; do
128+
if [ -f "$settings_file" ]; then
129+
echo "Scanning $settings_file for WebFetch domains..."
130+
WEBFETCH_DOMAINS="$WEBFETCH_DOMAINS $(extract_webfetch_domains "$settings_file")"
131+
fi
132+
done
133+
134+
UNIQUE_DOMAINS=$(printf '%s\n' "$WEBFETCH_DOMAINS" | tr ' ' '\n' | sed '/^$/d' | sort -u)
135+
if [ -n "$UNIQUE_DOMAINS" ]; then
136+
while read -r domain; do
137+
if [[ "$domain" == \** ]]; then
138+
echo "WARNING: Wildcard domain '$domain' cannot be resolved to IPs (skipping)"
139+
continue
140+
fi
141+
echo "Resolving WebFetch domain: $domain..."
142+
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
143+
if [ -z "$ips" ]; then
144+
echo "WARNING: Failed to resolve $domain (skipping)"
145+
continue
146+
fi
147+
while read -r ip; do
148+
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
149+
echo "WARNING: Invalid IP from DNS for $domain: $ip (skipping)"
150+
continue
151+
fi
152+
ipset add allowed-domains "$ip" 2>/dev/null || true
153+
done < <(echo "$ips")
154+
done <<< "$UNIQUE_DOMAINS"
155+
fi
156+
112157
# --- Host network detection ---
113158
HOST_IP=$(ip route | grep default | cut -d" " -f3)
114159
if [ -z "$HOST_IP" ]; then

docs/DECISIONS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,15 @@ When a decision is superseded or obsolete, delete it (git history preserves the
190190
- Deny `git remote add`, `set-url`, `remove`, `rename`, `set-head` in settings.json and all tier files -- read-only `git remote -v` remains allowed via the existing `Bash(git remote *)` allow rule
191191
- Deny rules are absolute in Claude Code (cannot be overridden by allow), making this the correct control layer vs hooks
192192
- Tier files use wildcard prefix `Bash(*git remote add *)` to catch chained command variants
193+
194+
## 2026-03-16: WebFetch Firewall Integration
195+
196+
**Request**: Connect the devcontainer iptables firewall to Claude Code's WebFetch permission settings so users don't need to manually edit the firewall script when working with external services.
197+
198+
**Decisions**:
199+
- Firewall reads `WebFetch(domain:...)` patterns from settings.json and settings.local.json at container startup -- single source of truth for domain whitelisting
200+
- Only `allow` and `ask` lists are scanned (not `deny`) -- denied domains should never be whitelisted
201+
- Bare `WebFetch` (no domain qualifier) is ignored -- it grants tool permission but has no domain to resolve
202+
- Wildcard domains (e.g., `*.example.com`) are skipped with a warning -- DNS cannot resolve wildcard patterns to IPs
203+
- Empty domain values filtered by `sed '/^$/d'` instead of `grep -v '^$'` -- grep exits non-zero on empty input under `set -euo pipefail`
204+
- WebFetch settings changes take effect on container restart (`init-firewall.sh` runs from `postStartCommand`); permission tier changes require rebuild (`onCreateCommand` copies tier to `settings.local.json`)

docs/DEVCONTAINER_PERMISSIONS.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Set the tier via `PERMISSION_TIER` environment variable before building the devc
1616

1717
Regardless of tier, these layers provide defense-in-depth:
1818

19-
- **Firewall (iptables)**: All egress blocked except ~10 whitelisted domains
19+
- **Firewall (iptables)**: All egress blocked except whitelisted domains (built-in + WebFetch settings)
2020
- **Non-root user**: Cannot install system packages or modify system files
2121
- **dangerous-actions-blocker.sh**: Blocks rm -rf, sudo, force push, DROP DATABASE, secrets in args
2222
- **output-secrets-scanner.sh**: Warns on leaked credentials in command output
@@ -47,6 +47,23 @@ Regardless of tier, these layers provide defense-in-depth:
4747
| `cd path && command` | Use absolute paths: `command /absolute/path` | Chained commands bypass glob-based permission checks |
4848
| `git remote add/set-url/remove/rename/set-head` | Ask the user to manage remotes | Prevents code exfiltration to unauthorized remotes |
4949

50+
## Firewall Configuration
51+
52+
The devcontainer firewall (`init-firewall.sh`) restricts all outbound traffic to a built-in allowlist plus domains from Claude Code permission settings.
53+
54+
**Built-in domains** (always allowed): PyPI, GitHub (via API CIDR ranges), Anthropic/Claude, VS Code Marketplace, uv/Astral, plus telemetry endpoints (`sentry.io`, `statsig.anthropic.com`, `statsig.com`).
55+
56+
**WebFetch domain auto-whitelisting**: The firewall scans `.claude/settings.json` and `.claude/settings.local.json` for `WebFetch(domain:...)` patterns in `allow` and `ask` lists. Matched domains are resolved via DNS and added to the ipset allowlist.
57+
58+
| Pattern | Firewall behavior |
59+
|---------|-------------------|
60+
| `WebFetch(domain:algoenergy.cz)` | Resolved and whitelisted |
61+
| `WebFetch(domain:*.example.com)` | Skipped (wildcards cannot be resolved) |
62+
| `WebFetch` (bare) | Ignored (no domain to resolve) |
63+
| `WebFetch(domain:)` (empty) | Filtered out |
64+
65+
Changes to WebFetch settings in `.claude/settings.json` or `.claude/settings.local.json` take effect on container restart. Changes to `.devcontainer/permissions/*.json` require a full rebuild (`devcontainer rebuild`).
66+
5067
## Tier Comparison
5168

5269
| Capability | Tier 1 | Tier 2 | Tier 3 |

0 commit comments

Comments
 (0)