Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
158 changes: 158 additions & 0 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
@@ -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."