-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Relax inbound firewall and add tamper protection #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,12 @@ IFS=$'\n\t' | |||||||||||||||||
| # Restricts egress to: PyPI, GitHub, Anthropic/Claude, VS Code, uv/Astral, | ||||||||||||||||||
| # plus any domains from WebFetch(domain:...) permission patterns. | ||||||||||||||||||
| # Uses ipset with aggregated CIDR ranges for reliable filtering. | ||||||||||||||||||
| # FIREWALL_ALLOW_INBOUND (default: true) controls inbound filtering. | ||||||||||||||||||
| # When true, INPUT chain is left permissive (Docker handles inbound isolation). | ||||||||||||||||||
| # When false, strict INPUT DROP policy is applied. | ||||||||||||||||||
|
|
||||||||||||||||||
| ALLOW_INBOUND="${FIREWALL_ALLOW_INBOUND:-true}" | ||||||||||||||||||
| echo "Inbound firewall mode: $([ "$ALLOW_INBOUND" = "true" ] && echo "permissive (Docker handles isolation)" || echo "strict (INPUT DROP)")" | ||||||||||||||||||
|
|
||||||||||||||||||
| echo "iptables version: $(iptables --version)" | ||||||||||||||||||
| if iptables_path="$(command -v iptables 2>/dev/null)"; then | ||||||||||||||||||
|
|
@@ -49,10 +55,14 @@ fi | |||||||||||||||||
|
|
||||||||||||||||||
| # Allow DNS and localhost before any restrictions | ||||||||||||||||||
| iptables -A OUTPUT -p udp --dport 53 -j ACCEPT | ||||||||||||||||||
| iptables -A INPUT -p udp --sport 53 -j ACCEPT | ||||||||||||||||||
| if [ "$ALLOW_INBOUND" != "true" ]; then | ||||||||||||||||||
| iptables -A INPUT -p udp --sport 53 -j ACCEPT | ||||||||||||||||||
| fi | ||||||||||||||||||
| iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT | ||||||||||||||||||
| iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT | ||||||||||||||||||
| iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT | ||||||||||||||||||
| if [ "$ALLOW_INBOUND" != "true" ]; then | ||||||||||||||||||
| iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT | ||||||||||||||||||
| fi | ||||||||||||||||||
| iptables -A INPUT -i lo -j ACCEPT | ||||||||||||||||||
| iptables -A OUTPUT -o lo -j ACCEPT | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -164,7 +174,9 @@ fi | |||||||||||||||||
| HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") | ||||||||||||||||||
| echo "Host network detected as: $HOST_NETWORK" | ||||||||||||||||||
|
|
||||||||||||||||||
| iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT | ||||||||||||||||||
| if [ "$ALLOW_INBOUND" != "true" ]; then | ||||||||||||||||||
| iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT | ||||||||||||||||||
| fi | ||||||||||||||||||
| iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT | ||||||||||||||||||
|
|
||||||||||||||||||
| # Block all IPv6 traffic (firewall is IPv4-only) | ||||||||||||||||||
|
|
@@ -175,7 +187,9 @@ ip6tables -A INPUT -i lo -j ACCEPT 2>/dev/null || true | |||||||||||||||||
| ip6tables -A OUTPUT -o lo -j ACCEPT 2>/dev/null || true | ||||||||||||||||||
|
|
||||||||||||||||||
| # Allow established connections | ||||||||||||||||||
| iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||||||||||||||||||
| if [ "$ALLOW_INBOUND" != "true" ]; then | ||||||||||||||||||
| iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||||||||||||||||||
| fi | ||||||||||||||||||
| iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||||||||||||||||||
|
|
||||||||||||||||||
| # Allow traffic to whitelisted domains | ||||||||||||||||||
|
|
@@ -185,11 +199,13 @@ iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT | |||||||||||||||||
| iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited | ||||||||||||||||||
|
|
||||||||||||||||||
| # Set default policies AFTER all ACCEPT rules (prevents lockout on partial failure) | ||||||||||||||||||
| iptables -P INPUT DROP | ||||||||||||||||||
| if [ "$ALLOW_INBOUND" != "true" ]; then | ||||||||||||||||||
| iptables -P INPUT DROP | ||||||||||||||||||
| fi | ||||||||||||||||||
|
Comment on lines
+202
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Permissive mode is not explicitly enforced (idempotency bug). When Proposed fix-if [ "$ALLOW_INBOUND" != "true" ]; then
- iptables -P INPUT DROP
-fi
+if [ "$ALLOW_INBOUND" != "true" ]; then
+ iptables -P INPUT DROP
+else
+ iptables -P INPUT ACCEPT
+fiBased on learnings: "Implement two-layer defense against data exfiltration: firewall (iptables whitelist in devcontainer) and exfiltration guard ( 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| iptables -P FORWARD DROP | ||||||||||||||||||
| iptables -P OUTPUT DROP | ||||||||||||||||||
|
|
||||||||||||||||||
| echo "Firewall configuration complete" | ||||||||||||||||||
| echo "Firewall configuration complete (inbound: $([ "$ALLOW_INBOUND" = "true" ] && echo "permissive" || echo "strict"))" | ||||||||||||||||||
|
|
||||||||||||||||||
| # --- Verification --- | ||||||||||||||||||
| echo "Verifying firewall rules..." | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -167,3 +167,13 @@ def test_checks_edit_and_write(self) -> None: | |
| content = (HOOKS_DIR / "auto-format.sh").read_text(encoding="utf-8") | ||
| assert '"Edit"' in content, "auto-format should check Edit tool" | ||
| assert '"Write"' in content, "auto-format should check Write tool" | ||
|
|
||
|
|
||
| class TestFirewallDenyRules: | ||
| """Verify firewall tampering is denied in settings.json.""" | ||
|
|
||
| def test_settings_denies_firewall_commands(self) -> None: | ||
| settings_path = Path(__file__).parent.parent / ".claude" / "settings.json" | ||
| content = settings_path.read_text(encoding="utf-8") | ||
| for pattern in ["iptables", "ip6tables", "ipset", "nft", "init-firewall"]: | ||
| assert pattern in content, f"settings.json missing firewall deny pattern: {pattern}" | ||
|
Comment on lines
+175
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test is too weak: it checks substrings, not actual deny-list entries. This can pass even if firewall patterns are moved out of Proposed fix+import json
@@
class TestFirewallDenyRules:
@@
def test_settings_denies_firewall_commands(self) -> None:
settings_path = Path(__file__).parent.parent / ".claude" / "settings.json"
- content = settings_path.read_text(encoding="utf-8")
- for pattern in ["iptables", "ip6tables", "ipset", "nft", "init-firewall"]:
- assert pattern in content, f"settings.json missing firewall deny pattern: {pattern}"
+ data = json.loads(settings_path.read_text(encoding="utf-8"))
+ deny_rules = set(data.get("permissions", {}).get("deny", []))
+ expected = {
+ "Bash(*iptables *)",
+ "Bash(*ip6tables *)",
+ "Bash(*ipset *)",
+ "Bash(*nft *)",
+ "Bash(*init-firewall*)",
+ }
+ for pattern in expected:
+ assert pattern in deny_rules, f"settings.json missing firewall deny pattern: {pattern}"Based on learnings: "Implement two-layer defense against data exfiltration: firewall (iptables whitelist in devcontainer) and exfiltration guard ( 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.