diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..940b5dd --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,119 @@ +ARG PYTHON_VERSION=3.12 +FROM python:${PYTHON_VERSION}-bookworm + +ARG TZ +ENV TZ="$TZ" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Core system utilities + less \ + procps \ + sudo \ + man-db \ + unzip \ + # Shell and terminal + zsh \ + fzf \ + # Text editors + vim \ + # Version control + git \ + gh \ + # Network utilities + ca-certificates \ + curl \ + wget \ + dnsutils \ + iproute2 \ + # Firewall and security tools + iptables \ + ipset \ + aggregate \ + # Build tools + make \ + build-essential \ + # Development utilities + jq \ + gnupg2 \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create non-root user +ARG USERNAME=dev +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Ensure user has access to shared directories +RUN mkdir -p /usr/local/share && \ + chown -R $USERNAME:$USERNAME /usr/local/share + +# Persist zsh history +RUN mkdir /commandhistory \ + && touch /commandhistory/.zsh_history \ + && chown -R $USERNAME /commandhistory + +# Set `DEVCONTAINER` environment variable to help with orientation +ENV DEVCONTAINER=true + +# Create workspace and config directories and set permissions +RUN mkdir -p /workspace /home/$USERNAME/.claude && \ + chown -R $USERNAME:$USERNAME /workspace /home/$USERNAME/.claude + +WORKDIR /workspace + +# Install git-delta +ARG GIT_DELTA_VERSION=0.18.2 +RUN ARCH=$(dpkg --print-architecture) && \ + wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + +# Switch to non-root user +USER $USERNAME + +# Install uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/home/$USERNAME/.local/bin:$PATH" + +# Set the default shell to zsh +ENV SHELL=/bin/zsh + +# Set the default editor +ENV EDITOR=vim +ENV VISUAL=vim + +# Install and configure zsh with powerline10k theme +ARG ZSH_IN_DOCKER_VERSION=1.2.0 + +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \ + -t gallifrey \ + -p git \ + -p gh \ + -p fzf \ + -p python \ + -a "export HISTFILE=/commandhistory/.zsh_history" \ + -a "export HISTSIZE=10000" \ + -a "export SAVEHIST=10000" \ + -a "setopt SHARE_HISTORY" \ + -x + +# Install Claude Code using the official installer +RUN curl -fsSL https://claude.ai/install.sh | bash + +# Copy and set up firewall script +COPY init-firewall.sh /usr/local/bin/ +USER root +RUN chmod +x /usr/local/bin/init-firewall.sh && \ + echo "$USERNAME ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/$USERNAME-firewall && \ + chmod 0440 /etc/sudoers.d/$USERNAME-firewall +USER $USERNAME + +# Configure git-delta as default pager for git +RUN git config --global core.pager delta && \ + git config --global interactive.diffFilter "delta --color-only" && \ + git config --global delta.navigate true && \ + git config --global delta.light false diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3ca7ee7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,63 @@ +{ + "name": "Claude Code Sandbox", + "build": { + "dockerfile": "Dockerfile", + "args": { + "TZ": "${localEnv:TZ:America/New_York}", + "GIT_DELTA_VERSION": "0.18.2", + "ZSH_IN_DOCKER_VERSION": "1.2.0", + "PYTHON_VERSION": "3.12" + } + }, + "runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"], + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code", + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "astral-sh.ty@prerelease", + "tamasfe.even-better-toml" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "python.defaultInterpreterPath": "/workspace/.venv/bin/python", + "python.terminal.activateEnvironment": true, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "zsh" + } + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + } + } + } + }, + "remoteUser": "dev", + "mounts": [ + "source=claude-code-history-${devcontainerId},target=/commandhistory,type=volume", + "source=claude-code-config-${devcontainerId},target=/home/dev/.claude,type=volume" + ], + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/dev/.claude", + "POWERLEVEL9K_DISABLE_GITSTATUS": "true" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + "postCreateCommand": "uv sync", + "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", + "waitFor": "postStartCommand" +} diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100644 index 0000000..dd1e9f4 --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,158 @@ +#!/bin/bash +set -e + +echo "๐Ÿ”ฅ Initializing firewall..." + +# Save Docker DNS nameserver before flushing rules +DOCKER_DNS=$(grep nameserver /etc/resolv.conf | head -n1 | awk '{print $2}') +echo "๐Ÿ“‹ Docker DNS: $DOCKER_DNS" + +# Flush existing rules +echo "๐Ÿงน Flushing existing rules..." +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X + +# Destroy and recreate ipset for allowed domains +ipset destroy allowed-domains 2>/dev/null || true +ipset create allowed-domains hash:ip + +# Function to resolve domain and add to ipset +add_domain() { + local domain=$1 + echo " ๐ŸŒ Resolving $domain..." + local ips=$(dig +short "$domain" @$DOCKER_DNS | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$') + if [ -z "$ips" ]; then + echo " โš ๏ธ Warning: Could not resolve $domain" + return + fi + for ip in $ips; do + echo " โœ“ Adding $ip ($domain)" + ipset add allowed-domains "$ip" 2>/dev/null || true + done +} + +# Function to add CIDR range to ipset +add_cidr() { + local cidr=$1 + echo " ๐Ÿ“ฆ Adding CIDR range: $cidr" + ipset add allowed-domains "$cidr" 2>/dev/null || true +} + +echo "๐Ÿ“ Building allowlist..." + +# Add Docker DNS +if [ -n "$DOCKER_DNS" ]; then + echo " ๐Ÿณ Adding Docker DNS: $DOCKER_DNS" + ipset add allowed-domains "$DOCKER_DNS" +fi + +# Fetch and add GitHub IP ranges +echo " ๐Ÿ™ Fetching GitHub IP ranges..." +GITHUB_IPS=$(curl -s https://api.github.com/meta | jq -r '.git[]' 2>/dev/null || echo "") +if [ -n "$GITHUB_IPS" ]; then + for cidr in $GITHUB_IPS; do + add_cidr "$cidr" + done +else + echo " โš ๏ธ Warning: Could not fetch GitHub IPs, adding fallback domains" + add_domain "github.com" + add_domain "api.github.com" +fi + +echo " ๐Ÿค– Adding Anthropic API domains..." +add_domain "api.anthropic.com" +add_domain "claude.ai" + +echo " ๐Ÿ”ง Adding VSCode and development tool domains..." +add_domain "marketplace.visualstudio.com" +add_domain "vscode-sync.trafficmanager.net" +add_domain "update.code.visualstudio.com" +add_domain "vscode.download.prss.microsoft.com" +add_domain "main.vscode-cdn.net" + +echo " ๐Ÿ“ฆ Adding VSCode extension gallery API domains..." +add_domain "anthropic.gallery.vsassets.io" +add_domain "ms-python.gallery.vsassets.io" +add_domain "charliermarsh.gallery.vsassets.io" +add_domain "astral-sh.gallery.vsassets.io" +add_domain "tamasfe.gallery.vsassets.io" + +echo " ๐Ÿ“ฆ Adding VSCode extension CDN domains..." +add_domain "anthropic.gallerycdn.vsassets.io" +add_domain "ms-python.gallerycdn.vsassets.io" +add_domain "charliermarsh.gallerycdn.vsassets.io" +add_domain "astral-sh.gallerycdn.vsassets.io" +add_domain "tamasfe.gallerycdn.vsassets.io" + +echo " ๐Ÿ Adding Python package index..." +add_domain "pypi.org" +add_domain "files.pythonhosted.org" + +echo " ๐ŸŒ Adding additional development resources..." +add_domain "astral.sh" +add_domain "github.com" +add_domain "raw.githubusercontent.com" + +# Detect host network +HOST_NETWORK=$(ip route | grep default | awk '{print $3}' | cut -d'.' -f1-3).0/24 +echo " ๐Ÿ  Host network detected: $HOST_NETWORK" +ipset add allowed-domains "$HOST_NETWORK" 2>/dev/null || true + +echo "๐Ÿ›ก๏ธ Setting up iptables rules..." + +# Set default policies to DROP +iptables -P INPUT DROP +iptables -P FORWARD DROP +iptables -P OUTPUT DROP + +# Allow localhost traffic +iptables -A INPUT -i lo -j ACCEPT +iptables -A OUTPUT -o lo -j ACCEPT + +# Allow established and related connections +iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow DNS queries to Docker DNS +iptables -A OUTPUT -p udp -d $DOCKER_DNS --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp -d $DOCKER_DNS --dport 53 -j ACCEPT + +# Allow SSH (if needed) +iptables -A INPUT -p tcp --dport 22 -j ACCEPT +iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT + +# Allow outbound traffic to allowed domains +iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT + +# Reject everything else with ICMP response for faster feedback +iptables -A OUTPUT -j REJECT --reject-with icmp-host-prohibited + +echo "โœ… Firewall configured successfully!" + +# Verify firewall is working +echo "๐Ÿงช Testing firewall..." + +# Test that we can reach GitHub API (should succeed) +if curl -s --max-time 5 https://api.github.com > /dev/null 2>&1; then + echo " โœ“ GitHub API accessible" +else + echo " โŒ ERROR: Cannot reach GitHub API (this should work)" + echo " โš ๏ธ Firewall verification failed!" +fi + +# Test that we cannot reach example.com (should fail) +if curl -s --max-time 5 https://example.com > /dev/null 2>&1; then + echo " โŒ ERROR: example.com is accessible (should be blocked)" + echo " โš ๏ธ Firewall verification failed!" +else + echo " โœ“ example.com blocked (as expected)" +fi + +echo "๐ŸŽ‰ Firewall initialization complete!" +echo "" +echo "โ„น๏ธ You can now run Claude Code with --dangerously-skip-permissions" +echo " The firewall restricts network access to approved destinations only."