Skip to content

Instantly share code, notes, and snippets.

@mmizutani
Last active June 13, 2025 09:59
Show Gist options
  • Save mmizutani/01d47aa43f0593bfd1c754d0898f7d5c to your computer and use it in GitHub Desktop.
Save mmizutani/01d47aa43f0593bfd1c754d0898f7d5c to your computer and use it in GitHub Desktop.
Dev Container post create command script to setup firewall for Claude Code (Antropic's original plus some enhancements)
#!/bin/bash
#
# init-firewall.sh - Egress Firewall for Development Container with Claude Code
#
# This script configures iptables to function as a strict egress firewall.
# Its primary purpose is to enhance security by restricting network access from
# within a development container to only a predefined list of essential services
# required for development, such as package managers (npm, Go), source control
# (GitHub), and specific APIs (Anthropic).
# Adapted from https://github.com/anthropics/devcontainer-features/blob/e09d1bb8d0f413fc8e6a5936478939cf2463deec/src/claude-code/init-firewall.sh
#
# Key Features:
# - Whitelist-based: All outbound traffic is blocked by default, and only
# connections to explicitly allowed domains are permitted.
# - Dynamic IP Sets: Uses ipset to manage the list of allowed IP addresses.
# It dynamically resolves domain names and fetches IP ranges for services
# like GitHub to keep the rules current.
# - Atomic Updates: The list of allowed IPs is built in a temporary ipset
# and then atomically swapped with the live one. This ensures the firewall
# is always using a complete and valid ruleset, preventing race conditions
# or incomplete rule application.
# - Reentrant & Robust: The script is designed to be safely re-runnable. It
# includes error handling and cleanup traps to manage failures gracefully.
#
# Prerequisites:
# - Must be run with root privileges (e.g., 'sudo').
# - The following packages/commands must be installed and in the system's PATH:
# - iproute2 (provides the 'ip' command)
# - iptables
# - ipset
# - curl
# - jq
# - dnsutils (provides the 'dig' command)
# - aggregate (a tool for merging CIDR blocks, may require manual installation)
#
# Usage:
# This script is intended to be run at container startup, for example via the
# "postCreateCommand" in a devcontainer.json configuration. It must be run as
# root because it modifies the system's firewall rules.
#
# To enable the firewall, COPY this script to `/usr/local/bin/init-firewall.sh` and add
# the following to your devcontainer.json:
# {
# ...,
# "runArgs": [
# "--cap-add=NET_ADMIN",
# "--cap-add=NET_RAW"
# ],
# "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh"
# }
set -eu -o pipefail -o errtrace
shopt -s inherit_errexit nullglob
# --- Cleanup and Error Handling ---
# This function is registered with a 'trap' to be called on any script exit.
# Its purpose is to ensure that the temporary ipset used for building the new
# ruleset is always removed, even if the script is interrupted or fails.
function final_cleanup() {
# Attempt to destroy the temporary ipset, ignoring errors if it doesn't exist.
ipset destroy allowed-domains-new 2>/dev/null || true
}
# This function is registered with a 'trap' to be called on any error (non-zero exit code).
# It provides a clear message indicating that the firewall setup has failed, which
# is critical because it leaves the container in a potentially insecure state
# (likely with open firewall policies from the initial setup phase).
function error_handler() {
local exit_code=$?
echo
echo "--- ERROR: Firewall script failed with exit code $exit_code ---"
echo "--- Firewall may be in an inconsistent (likely open) state ---"
# Call final_cleanup to remove any temporary artifacts.
final_cleanup
}
# Register the functions with their respective trap signals.
# ERR: Triggered by any command failing.
# EXIT: Triggered when the script exits, regardless of success or failure.
trap error_handler ERR
trap final_cleanup EXIT
# --- Initial State Reset ---
# Temporarily set default policies to ACCEPT. This is a critical step for
# reentrancy. If the script was run before and set the policies to DROP, this
# script would fail when it tries to use 'curl' or 'dig' to fetch IP addresses.
# We reset to ACCEPT here, and then set them back to a secure DROP at the end.
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
# Flush (F) all existing rules from the filter table and delete (X) all non-default chains.
# This ensures we are starting from a clean slate.
# We only touch the 'filter' table, which is the default for iptables commands.
# We specifically AVOID flushing the 'nat' or 'mangle' tables, as this can
# interfere with Docker's internal networking and DNS resolution.
iptables -F
iptables -X
# --- Base Rule Configuration ---
# Allow essential network traffic before applying any restrictions.
# Allow outbound DNS requests (to resolve domain names).
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Allow inbound responses to our DNS requests.
iptables -A INPUT -p udp --sport 53 -j ACCEPT
# Allow outbound SSH connections (e.g., for git over SSH).
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
# Allow inbound responses to our SSH connections. The 'state' module ensures
# we only accept packets that are part of an established connection.
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow all traffic on the loopback interface (for services running on localhost).
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# --- Dynamic IP Whitelist Creation ---
# Create a new, temporary ipset. We will populate this set with all the
# allowed IPs. Using a temporary set is key to the atomic update strategy.
TMP_IPSET="allowed-domains-new"
ipset create "$TMP_IPSET" hash:net
# Fetch GitHub's IP ranges from their metadata API.
# The 'jq' command parses the JSON response and extracts the IP blocks
# for web traffic, the API, and git operations.
# 'aggregate' is a tool that merges overlapping CIDR blocks.
echo "Fetching GitHub IP ranges..."
gh_ranges=$(curl -s https://api.github.com/meta)
if [ -z "$gh_ranges" ]; then
echo "ERROR: Failed to fetch GitHub IP ranges"
exit 1
fi
if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
echo "ERROR: GitHub API response missing required fields"
exit 1
fi
echo "Processing GitHub IPs..."
while read -r cidr; do
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
exit 1
fi
echo "Adding GitHub range $cidr"
ipset add "$TMP_IPSET" "$cidr"
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
# Resolve a list of the required domains for Cloud Code CLI. For each domain, we use 'dig' to
# get its A records (IP addresses) and add them to our temporary ipset.
# This list should be maintained to include all external services the
# development environment needs to contact.
for domain in \
"registry.npmjs.org" \
"api.anthropic.com" \
"sentry.io" \
"statsig.anthropic.com" \
"statsig.com" \
"proxy.golang.org" \
"sum.golang.org" \
"index.golang.org" \
; do
echo "Resolving $domain..."
ips=$(dig +short A "$domain")
if [ -z "$ips" ]; then
echo "ERROR: Failed to resolve $domain"
exit 1
fi
while read -r ip; do
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "ERROR: Invalid IP from DNS for $domain: $ip"
exit 1
fi
echo "Adding $ip for $domain"
ipset add "$TMP_IPSET" "$ip"
done < <(echo "$ips")
done
# --- Atomic IP Set Swap ---
# This is the core of the atomic update. The 'ipset swap' command is
# instantaneous. It replaces the live 'allowed-domains' set with our newly
# populated temporary set. This avoids any period of time where the firewall
# might have an incomplete or empty list of IPs.
echo "Atomically updating the firewall ipset..."
# Ensure the target set exists before trying to swap.
ipset create allowed-domains hash:net -exist
ipset swap "$TMP_IPSET" allowed-domains
echo "IP set updated."
# --- Final Firewall Policy Application ---
# Get the container's host IP address from the default route. This is used
# to allow communication with services running on the host machine.
HOST_IP=$(ip route | grep default | cut -d" " -f3)
if [ -z "$HOST_IP" ]; then
echo "ERROR: Failed to detect host IP"
exit 1
fi
# Derive the host's local network (assuming a /24 subnet) and allow
# all traffic to and from that network.
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
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
# Set the default policies for all chains to DROP. This is the "whitelist"
# enforcement. Any packet that does not match an explicit ACCEPT rule above
# will be dropped.
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# Allow established and related connections. This is a crucial stateful firewall
# rule. It allows return traffic for connections that we have initiated (e.g.,
# the response from a web server we contacted). Without this, no connections
# would work.
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# This is the main egress rule. It allows any outbound packet (-A OUTPUT)
# whose destination IP address (--match-set allowed-domains dst) is in our ipset.
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
# --- Verification ---
echo "Firewall configuration complete"
echo "Verifying firewall rules..."
# Test 1: Try to reach a domain that should be BLOCKED.
# We expect this curl command to fail (timeout). If it succeeds, the firewall
# is not working correctly, and we exit with an error.
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - was able to reach https://example.com"
exit 1
else
echo "Firewall verification passed - unable to reach https://example.com as expected"
fi
# Test 2: Try to reach a domain that should be ALLOWED.
# We expect this to succeed. If it fails, something is wrong with our ruleset
# or the setup process.
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
exit 1
else
echo "Firewall verification passed - able to reach https://api.github.com as expected"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment