Skip to content
Closed
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
66 changes: 66 additions & 0 deletions container/gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
FROM python:3.11-slim

# Install git, gh CLI, and Squid proxy for network lockdown
RUN apt-get update && apt-get install -y \
git curl gnupg \
# Squid HTTP proxy with SSL support for SNI inspection
squid-openssl \
# OpenSSL for certificate generation
openssl \
# gosu for dropping privileges (Squid needs root start, gateway needs non-root)
gosu \
&& \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \
dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] \
https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \
apt-get update && apt-get install -y gh && \
rm -rf /var/lib/apt/lists/*

# Generate self-signed certificate for Squid SSL bump (peek/splice, not MITM)
# This certificate is used only to inspect the SNI field, not to decrypt traffic
# Using 10-year validity (3650 days) since this is internal-only and doesn't
# require the security properties of short-lived certificates (no chain of trust)
RUN mkdir -p /etc/squid/ssl /var/log/squid /var/spool/squid && \
openssl req -new -newkey rsa:2048 -sha256 -days 3650 -nodes -x509 \
-subj "/CN=egg-gateway-proxy/O=egg/C=US" \
-keyout /etc/squid/squid-ca.pem \
-out /etc/squid/squid-ca.pem && \
# Squid runs as proxy user - ensure it can read the certificate
chown proxy:proxy /etc/squid/squid-ca.pem && \
chmod 400 /etc/squid/squid-ca.pem && \
chown -R proxy:proxy /var/log/squid /var/spool/squid && \
# Initialize Squid cache directories
/usr/sbin/squid -z -N 2>/dev/null || true

# Create sandbox user with UID/GID 1000 so gosu can resolve HOME correctly
# Without this, gosu sets HOME=/ when dropping to UID 1000, breaking Path.home()
# This matches the sandbox container user setup for consistency
RUN groupadd -g 1000 sandbox && \
useradd -m -u 1000 -g 1000 -s /bin/bash sandbox

WORKDIR /app

# Copy gateway Python modules
COPY gateway/*.py ./gateway/
COPY shared/*.py ./shared/

# Copy Squid configuration for network lockdown
# squid.conf is the restrictive allowlist-based config (used in private mode)
# squid-allow-all.conf allows full internet access (used in public mode)
COPY container/gateway/squid.conf /etc/squid/squid.conf
COPY container/gateway/squid-allow-all.conf /etc/squid/squid-allow-all.conf
COPY container/gateway/allowed_domains.txt /etc/squid/allowed_domains.txt
RUN chmod 644 /etc/squid/squid.conf /etc/squid/squid-allow-all.conf /etc/squid/allowed_domains.txt

# Install dependencies
RUN pip install --no-cache-dir flask waitress pyyaml requests PyJWT cryptography

ENV PYTHONPATH="/app"
# Expose both gateway API port and Squid proxy port
EXPOSE 9847 3128

COPY container/gateway/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
24 changes: 24 additions & 0 deletions container/gateway/allowed_domains.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Allowed domains for network lockdown (private mode)
# These are the only domains the sandbox container can access through the proxy
#
# Format: One domain per line, supports subdomains via .domain.com syntax
# Lines starting with # are comments

# Anthropic Claude API
api.anthropic.com

# IMPORTANT: GitHub domains are intentionally NOT in this allowlist.
# All GitHub/git access MUST go through the gateway sidecar's git/gh wrappers
# (via REST API at egg-gateway:9847), not directly through the proxy.
# This ensures policy enforcement (branch ownership, merge blocking, etc.)
# cannot be bypassed by direct API calls.
#
# The gateway sidecar holds the GitHub token and enforces:
# - Branch ownership policy (agent can only push to owned branches)
# - Merge blocking (agent cannot merge PRs - human must merge via GitHub UI)
# - Force push blocking (agent cannot force push)
# - Audit logging for all operations

# Note: PyPI, npm, and other package managers are intentionally NOT included
# All dependencies must be pre-installed in the Docker image
# This prevents supply chain attacks and ensures reproducible builds
121 changes: 121 additions & 0 deletions container/gateway/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash
set -e

# =============================================================================
# Gateway Sidecar Entrypoint
#
# Starts the gateway API server and Squid proxy for network filtering.
#
# The gateway always runs with locked-down Squid (allows only api.anthropic.com).
# Per-container mode is enforced at the container level:
# - Private containers: Use isolated network + route through this proxy
# - Public containers: Use external network + bypass proxy (direct internet)
#
# This allows private and public containers to run simultaneously without
# gateway restarts.
# =============================================================================

echo "=== Egg Gateway Sidecar Starting (Per-Container Mode Architecture) ==="
echo " Squid: Locked (api.anthropic.com only)"
echo " Private containers: Use proxy on isolated network"
echo " Public containers: Bypass proxy on external network"
echo ""

# Always use locked-down Squid (only private containers route through it)
# Note: PRIVATE_MODE env var is no longer used - mode is per-container via sessions
SQUID_CONF="/etc/squid/squid.conf"

# Note: GitHub tokens are now managed in-memory by token_refresher.py
# We only need to verify the launcher secret is mounted
if [ ! -f "/secrets/launcher-secret" ]; then
echo "ERROR: /secrets/launcher-secret not mounted"
exit 1
fi

# Export launcher secret for authentication
export EGG_LAUNCHER_SECRET=$(cat /secrets/launcher-secret)

# =============================================================================
# Start Squid Proxy for Network Filtering
# =============================================================================

echo "Starting Squid proxy for network filtering..."
echo "Using config: $SQUID_CONF"

# Ensure log and spool directories exist and are writable
# Note: We may not have permission to chown (running as non-root), so we
# check writability directly and configure Squid to run with current user.
mkdir -p /var/log/squid /var/spool/squid

# Try to set ownership for squid's preferred user, but don't fail if we can't
if chown -R proxy:proxy /var/log/squid /var/spool/squid 2>/dev/null; then
echo " Log directories owned by proxy:proxy"
else
# Running as non-root - verify directories are writable
if [ -w /var/log/squid ] && [ -w /var/spool/squid ]; then
echo " Log directories writable by current user ($(id -un))"
else
echo "WARNING: Log directories may not be writable - Squid logging may fail"
fi
fi

# Initialize cache directories if needed
if [ ! -d "/var/spool/squid/00" ]; then
/usr/sbin/squid -z -N 2>/dev/null || true
fi

# Verify Squid configuration exists
if [ ! -f "$SQUID_CONF" ]; then
echo "ERROR: Squid configuration not found: $SQUID_CONF"
exit 1
fi
# Only check allowed_domains.txt in lockdown mode (not used in allow-all mode)
if [ "$SQUID_CONF" = "/etc/squid/squid.conf" ] && [ ! -f "/etc/squid/allowed_domains.txt" ]; then
echo "ERROR: Allowed domains file not found: /etc/squid/allowed_domains.txt"
exit 1
fi

# Start Squid in daemon mode
/usr/sbin/squid -f "$SQUID_CONF"

# Wait for Squid to start
elapsed=0
max_wait=30
while [ $elapsed -lt $max_wait ]; do
if /usr/sbin/squid -k check 2>/dev/null; then
echo "Squid proxy started successfully on port 3128"
break
fi
sleep 1
elapsed=$((elapsed + 1))
echo "Waiting for Squid to start... ($elapsed/$max_wait)"
done

if [ $elapsed -ge $max_wait ]; then
echo "ERROR: Squid failed to start within $max_wait seconds"
cat /var/log/squid/cache.log 2>/dev/null || true
exit 1
fi

# =============================================================================
# Start Gateway API Server
# =============================================================================

echo "Starting gateway API server on port 9847..."

# Run gateway on all interfaces (for container networking)
# Use exec to replace shell process with Python for proper signal handling
#
# If HOST_UID/HOST_GID are set, drop privileges using gosu before starting
# the Python gateway. This is required because:
# - Container starts as root so Squid can read its certificate
# - Gateway Python code must run as host user to avoid root-owned git files
if [ -n "${HOST_UID:-}" ] && [ -n "${HOST_GID:-}" ] && [ "$(id -u)" = "0" ]; then
echo "Dropping privileges to UID=$HOST_UID GID=$HOST_GID"
# Explicitly set HOME before gosu (consistent with sandbox container entrypoint)
# This ensures Path.home() resolves correctly in token_refresher.py
export HOME=/home/sandbox
exec gosu "$HOST_UID:$HOST_GID" python3 -m gateway.gateway --host 0.0.0.0 --port 9847
else
exec python3 -m gateway.gateway --host 0.0.0.0 --port 9847
fi
157 changes: 157 additions & 0 deletions container/gateway/squid-allow-all.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Squid proxy configuration for "Public Mode" (PRIVATE_MODE=false)
#
# This configuration allows all domain access while still routing traffic
# through the proxy for audit logging.
#
# SECURITY MODEL:
# This config is used when PRIVATE_MODE=false (public mode), which provides:
# - Full internet access (all domains allowed)
# - Public repos only (private repos blocked)
#
# The single PRIVATE_MODE flag ensures you can't accidentally combine open
# network with private repo access (a security anti-pattern).
#
# Key properties:
# - All domains are allowed through the proxy
# - Direct IP connections are still blocked (security)
# - All traffic is logged for audit
# - Repository access control handled by gateway API (PRIVATE_MODE=false)

# ==============================================================================
# Port Configuration
# ==============================================================================

# HTTP/HTTPS proxy port with SSL bump for SNI inspection
# The certificate is used for peek/splice operations, NOT for MITM
# Note: generate-host-certificates=off because we only peek/splice (no MITM)
# This avoids needing to initialize the ssl_db certificate database
http_port 3128 ssl-bump \
cert=/etc/squid/squid-ca.pem \
generate-host-certificates=off \
dynamic_cert_mem_cache_size=4MB

# ==============================================================================
# Access Control Lists
# ==============================================================================

# Define local network (egg-isolated subnet)
acl localnet src 172.30.0.0/24

# Block direct IP connections (bypass attempts)
# This prevents connections to IP addresses instead of hostnames.
# Multiple formats must be blocked to prevent bypass attacks:
# - Standard IPv4 decimal: 192.168.1.1
# - IPv6 in brackets: [2607:f8b0:4004:800::200e]
# - Octal notation: 0177.0.0.1 (equals 127.0.0.1)
# - Hexadecimal notation: 0x7f.0x00.0x00.0x01
# - Integer notation: 2130706433 (9-10 digit number)
acl direct_ipv4 url_regex ^https?://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+
acl direct_ipv6 url_regex ^https?://\[
acl direct_ip_octal url_regex ^https?://0[0-7]+\.
acl direct_ip_hex url_regex ^https?://0x[0-9a-fA-F]+
acl direct_ip_int url_regex ^https?://[0-9]{9,10}(/|$|:)

# CONNECT method (used for HTTPS tunneling)
acl CONNECT method CONNECT

# ==============================================================================
# SSL Bump Rules (SNI Inspection)
# ==============================================================================

# Step 1: Peek at the TLS ClientHello to read SNI
acl step1 at_step SslBump1

# Peek at SNI, then splice all connections (allow all domains)
# In allow-all mode, we splice everything for maximum compatibility
ssl_bump peek step1
ssl_bump splice all

# ==============================================================================
# HTTP Access Rules
# ==============================================================================

# Block direct IP connections (security: prevent SNI bypass)
# All IP address formats are blocked to prevent circumvention
http_access deny direct_ipv4
http_access deny direct_ipv6
http_access deny direct_ip_octal
http_access deny direct_ip_hex
http_access deny direct_ip_int

# Allow CONNECT from local network (HTTPS tunneling to any domain)
http_access allow CONNECT localnet

# Allow HTTP from local network (to any domain)
http_access allow localnet

# Deny everything else
http_access deny all

# ==============================================================================
# Logging Configuration
# ==============================================================================

# Structured logging for audit purposes
# Format: timestamp duration client_ip status_code bytes method url hierarchy_code mime_type
logformat squid_json {"timestamp":"%{%Y-%m-%dT%H:%M:%S}tl.%03tu","duration_ms":%tr,"client_ip":"%>a","status":%>Hs,"bytes":%<st,"method":"%rm","url":"%ru","hierarchy":"%Sh","mime_type":"%mt"}

# Log to stdout for Docker log collection
access_log stdio:/proc/self/fd/1 squid_json

# Also log to file for persistence
access_log /var/log/squid/access.log squid_json

# Cache manager access log
cache_log /var/log/squid/cache.log

# ==============================================================================
# Performance & Behavior
# ==============================================================================

# Disable caching (we're a filtering proxy, not a caching proxy)
cache deny all

# Connection timeouts
connect_timeout 30 seconds
read_timeout 60 seconds
request_timeout 60 seconds

# Graceful shutdown timeout
shutdown_lifetime 5 seconds

# DNS settings - use Docker's embedded DNS resolver
# This avoids leaking DNS queries to external services and ensures
# consistent resolution within the Docker network topology
dns_nameservers 127.0.0.11

# Disable ICP (Inter-Cache Protocol) - not needed
icp_port 0

# Workaround for Asahi Linux 16KB page size crash
# Without this, Squid fails with: "xcalloc: Unable to allocate 1073741816 blocks"
# See: https://bugs.launchpad.net/ubuntu-docker-images/+bug/1978272
max_filedescriptors 1024

# Limit maximum request size (prevent abuse)
request_body_max_size 100 MB

# Visible hostname for error pages
visible_hostname egg-gateway-proxy

# ==============================================================================
# Error Pages
# ==============================================================================

# Custom error message for blocked requests (IP addresses only in this mode)
deny_info ERR_ACCESS_DENIED all

# ==============================================================================
# Security Hardening
# ==============================================================================

# Don't forward internal headers
via off
forwarded_for delete

# Disable potentially dangerous features
httpd_suppress_version_string on
Loading
Loading