feat(docker): add isolated Claude Code environment with proxy sidecar
Two-container setup: claude (UID 1000, internal-only network) and proxy (Squid, UID 13). The internal Docker network uses internal: true so the claude container has no direct internet route. All egress is tunnelled through the Squid sidecar which enforces a domain allowlist. Both containers drop all capabilities and set no-new-privileges. claude.sh provides start/stop/run/update/logs/status/shell lifecycle management.
This commit is contained in:
commit
e0e5e03e58
10 changed files with 554 additions and 0 deletions
8
.dockerignore
Normal file
8
.dockerignore
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
|
README.md
|
||||||
|
claude.sh
|
||||||
|
.gitignore
|
||||||
|
.env.example
|
||||||
|
.dockerignore
|
||||||
9
.env.example
Normal file
9
.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Copy this file to .env and fill in your values.
|
||||||
|
# .env is git-ignored — never commit it.
|
||||||
|
|
||||||
|
# Required: your Anthropic API key
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
|
||||||
|
# Optional: mount a host directory as /workspace inside the Claude container.
|
||||||
|
# If unset, a named Docker volume is used (fully isolated from the host).
|
||||||
|
# WORKSPACE_DIR=/absolute/path/to/your/project
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
94
CLAUDE.md
Normal file
94
CLAUDE.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Project Guidance
|
||||||
|
|
||||||
|
This file provides context and guidance for working with this project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**docker-claude** runs Claude Code inside a hardened Docker environment with a Squid proxy sidecar. The goal is full host encapsulation: Claude cannot access the host filesystem or network. All egress is routed through an allowlist-enforcing proxy.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Two containers managed by Docker Compose:
|
||||||
|
|
||||||
|
- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network
|
||||||
|
- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist
|
||||||
|
|
||||||
|
Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-claude/
|
||||||
|
├── claude.sh # Control script: start / stop / run / update / logs / status / shell
|
||||||
|
├── docker-compose.yml # Service definitions and network topology
|
||||||
|
├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000)
|
||||||
|
├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13)
|
||||||
|
├── proxy/
|
||||||
|
│ └── squid.conf # Squid ACL config — egress allowlist lives here
|
||||||
|
├── .env.example # Template for ANTHROPIC_API_KEY
|
||||||
|
├── .gitignore # Excludes .env and logs
|
||||||
|
├── .dockerignore # Keeps .env out of build context
|
||||||
|
└── README.md # User documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x claude.sh
|
||||||
|
cp .env.example .env # set ANTHROPIC_API_KEY
|
||||||
|
./claude.sh start # build + start proxy + launch Claude interactively
|
||||||
|
./claude.sh update # rebuild images (no cache) after upstream updates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
- Shell scripts use `set -euo pipefail`
|
||||||
|
- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer
|
||||||
|
- No capabilities granted; `no-new-privileges` on all containers
|
||||||
|
- `.env` is never committed (enforced by `.gitignore` and `.dockerignore`)
|
||||||
|
- Commit messages follow **Angular format**: `type(scope): summary`
|
||||||
|
|
||||||
|
## Extending the Egress Allowlist
|
||||||
|
|
||||||
|
Add domains to `proxy/squid.conf` under the `allowed_sites` ACL, then rebuild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -p claude-secure build proxy
|
||||||
|
./claude.sh stop && ./claude.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Persistence
|
||||||
|
|
||||||
|
This sandbox has a persistent environment file at `/etc/sandbox-persistent.sh`.
|
||||||
|
|
||||||
|
This file is automatically sourced in all shell contexts:
|
||||||
|
- **Non-interactive shells**: via `BASH_ENV=/etc/sandbox-persistent.sh`
|
||||||
|
- **Login shells**: via `/etc/profile.d/sandbox-persistent.sh`
|
||||||
|
- **Interactive shells**: via `/etc/bash.bashrc` and `~/.bashrc`
|
||||||
|
|
||||||
|
## Critical: Shell Completions Must NOT Be in the Persistent Environment File
|
||||||
|
|
||||||
|
**NEVER add shell completion scripts to `/etc/sandbox-persistent.sh`.**
|
||||||
|
|
||||||
|
Shell completion scripts will completely break the bash tool when sourced via the persistent environment file.
|
||||||
|
|
||||||
|
## Network access
|
||||||
|
|
||||||
|
There is a firewall in place to restrict outbound network access. If you need http/https access to
|
||||||
|
an external service, request it by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sbx policy allow network [domain]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Authentication
|
||||||
|
|
||||||
|
The sandbox proxy handles GitHub authentication automatically by injecting credentials for HTTPS Git operations.
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
- Always read relevant files before making changes
|
||||||
|
- Run tests after making modifications
|
||||||
|
- Follow the existing code structure and patterns
|
||||||
|
- You have sudo permissions, so you can install necessary packages
|
||||||
|
- npm, pip and uv are already available for package management
|
||||||
30
Dockerfile.claude
Normal file
30
Dockerfile.claude
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install minimal runtime dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
bash \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN groupadd -g 1000 claude \
|
||||||
|
&& useradd -u 1000 -g claude -m -s /bin/bash claude
|
||||||
|
|
||||||
|
# Install Claude Code globally (runs as root for npm -g, then drops)
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
|
# Workspace directory owned by claude user
|
||||||
|
RUN mkdir -p /workspace && chown claude:claude /workspace
|
||||||
|
|
||||||
|
USER claude
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Proxy traffic through sidecar — override at runtime if needed
|
||||||
|
ENV HTTP_PROXY=http://proxy:3128
|
||||||
|
ENV HTTPS_PROXY=http://proxy:3128
|
||||||
|
ENV ALL_PROXY=http://proxy:3128
|
||||||
|
ENV NO_PROXY=localhost,127.0.0.1
|
||||||
|
|
||||||
|
ENTRYPOINT ["claude"]
|
||||||
25
Dockerfile.proxy
Normal file
25
Dockerfile.proxy
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
squid \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Give the proxy system user (UID 13) ownership of all Squid paths
|
||||||
|
RUN mkdir -p /var/spool/squid /var/log/squid \
|
||||||
|
&& chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid
|
||||||
|
|
||||||
|
COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf
|
||||||
|
|
||||||
|
USER proxy
|
||||||
|
|
||||||
|
# Initialise cache directories as the proxy user
|
||||||
|
RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true
|
||||||
|
|
||||||
|
EXPOSE 3128
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
|
||||||
|
CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128'
|
||||||
|
|
||||||
|
CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"]
|
||||||
120
README.md
Normal file
120
README.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# docker-claude
|
||||||
|
|
||||||
|
Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot reach the host filesystem or network directly.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Host machine │
|
||||||
|
│ │
|
||||||
|
│ claude.sh (control script) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────┐ │
|
||||||
|
│ │ Docker: claude-secure │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────┐ claude-internal │ │
|
||||||
|
│ │ │ claude │◄─────(internal only)───► │ │
|
||||||
|
│ │ │ (UID 1000) │ │ │ │
|
||||||
|
│ │ └─────────────┘ ┌──────┴──────┐ │ │
|
||||||
|
│ │ │ proxy │ │ │
|
||||||
|
│ │ │ (UID 13) │ │ │
|
||||||
|
│ │ └──────┬──────┘ │ │
|
||||||
|
│ │ proxy-external │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ internet (allowlisted) │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route)
|
||||||
|
- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist
|
||||||
|
- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly
|
||||||
|
- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine 24+
|
||||||
|
- Docker Compose v2 plugin (`docker compose version`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone / copy this repo
|
||||||
|
git clone <repo> docker-claude && cd docker-claude
|
||||||
|
|
||||||
|
# 2. Configure your API key
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env # set ANTHROPIC_API_KEY
|
||||||
|
|
||||||
|
# 3. Make the control script executable
|
||||||
|
chmod +x claude.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build images, start proxy, launch Claude Code interactively
|
||||||
|
./claude.sh start
|
||||||
|
|
||||||
|
# Same as start but skips image rebuild (faster on subsequent runs)
|
||||||
|
./claude.sh run
|
||||||
|
|
||||||
|
# Stop and remove all containers (proxy + any running sessions)
|
||||||
|
./claude.sh stop
|
||||||
|
|
||||||
|
# Rebuild images without cache (e.g. after Claude Code updates)
|
||||||
|
./claude.sh update
|
||||||
|
|
||||||
|
# Tail proxy access logs
|
||||||
|
./claude.sh logs
|
||||||
|
|
||||||
|
# Show container status
|
||||||
|
./claude.sh status
|
||||||
|
|
||||||
|
# Open a debug bash shell inside the Claude container
|
||||||
|
./claude.sh shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with host files
|
||||||
|
|
||||||
|
By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host.
|
||||||
|
|
||||||
|
To mount a specific host directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WORKSPACE_DIR=$HOME/myproject ./claude.sh run
|
||||||
|
```
|
||||||
|
|
||||||
|
The directory is mounted at `/workspace` inside the container.
|
||||||
|
|
||||||
|
## Egress allowlist
|
||||||
|
|
||||||
|
Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL:
|
||||||
|
|
||||||
|
```squid
|
||||||
|
acl allowed_sites dstdomain api.anthropic.com
|
||||||
|
acl allowed_sites dstdomain statsig.anthropic.com
|
||||||
|
# acl allowed_sites dstdomain api.github.com # uncomment if needed
|
||||||
|
# acl allowed_sites dstdomain registry.npmjs.org
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebuild the proxy after changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -p claude-secure build proxy
|
||||||
|
./claude.sh stop && ./claude.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security controls
|
||||||
|
|
||||||
|
| Control | Claude container | Proxy container |
|
||||||
|
|---|---|---|
|
||||||
|
| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) |
|
||||||
|
| `no-new-privileges` | yes | yes |
|
||||||
|
| All capabilities dropped | yes | yes |
|
||||||
|
| Direct internet access | no (`internal` network only) | allowlisted only |
|
||||||
|
| Host filesystem | no mounts by default | none |
|
||||||
|
| Docker socket | not mounted | not mounted |
|
||||||
163
claude.sh
Executable file
163
claude.sh
Executable file
|
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# claude.sh — Manage the isolated Claude Code Docker environment
|
||||||
|
# Usage: ./claude.sh <command> [args]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
|
||||||
|
PROJECT="claude-secure"
|
||||||
|
|
||||||
|
# ─── Colours ──────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[-]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
# ─── Dependency check ─────────────────────────────────────────────────────────
|
||||||
|
check_deps() {
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
error "Docker is not installed. https://docs.docker.com/get-docker/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! docker compose version &>/dev/null 2>&1; then
|
||||||
|
error "Docker Compose v2 plugin is required."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Environment loading ──────────────────────────────────────────────────────
|
||||||
|
load_env() {
|
||||||
|
local env_file="$SCRIPT_DIR/.env"
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source "$env_file"; set +a
|
||||||
|
fi
|
||||||
|
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
||||||
|
error "ANTHROPIC_API_KEY is not set."
|
||||||
|
error "Copy .env.example → .env and add your key, or export it in your shell."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Workspace volume resolution ──────────────────────────────────────────────
|
||||||
|
# Default: named Docker volume (fully isolated).
|
||||||
|
# Override: export WORKSPACE_DIR=/path/to/project before running.
|
||||||
|
workspace_flag() {
|
||||||
|
if [[ -n "${WORKSPACE_DIR:-}" ]]; then
|
||||||
|
local abs
|
||||||
|
abs="$(realpath "${WORKSPACE_DIR}")"
|
||||||
|
if [[ ! -d "$abs" ]]; then
|
||||||
|
error "WORKSPACE_DIR does not exist: $abs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "--volume ${abs}:/workspace:z"
|
||||||
|
else
|
||||||
|
echo "--volume ${PROJECT}-workspace:/workspace"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Compose wrapper ──────────────────────────────────────────────────────────
|
||||||
|
dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; }
|
||||||
|
|
||||||
|
# ─── Commands ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
check_deps
|
||||||
|
load_env
|
||||||
|
info "Building images..."
|
||||||
|
dc build
|
||||||
|
info "Starting proxy sidecar..."
|
||||||
|
dc up -d proxy
|
||||||
|
info "Waiting for proxy health check..."
|
||||||
|
dc up -d proxy # no-op if already healthy; compose waits via depends_on
|
||||||
|
info "Launching Claude Code..."
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
dc run --rm $(workspace_flag) claude "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
check_deps
|
||||||
|
info "Stopping all containers..."
|
||||||
|
dc down
|
||||||
|
info "Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_run() {
|
||||||
|
check_deps
|
||||||
|
load_env
|
||||||
|
info "Ensuring proxy is running..."
|
||||||
|
dc up -d proxy
|
||||||
|
info "Launching Claude Code..."
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
dc run --rm $(workspace_flag) claude "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
check_deps
|
||||||
|
info "Rebuilding images (no cache)..."
|
||||||
|
dc build --no-cache
|
||||||
|
info "Update complete. Run './claude.sh start' to launch."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
check_deps
|
||||||
|
local svc="${1:-proxy}"
|
||||||
|
dc logs -f "$svc"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
check_deps
|
||||||
|
dc ps
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_shell() {
|
||||||
|
check_deps
|
||||||
|
load_env
|
||||||
|
warn "Opening debug shell inside Claude container (non-Claude entrypoint)."
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
dc run --rm --entrypoint /bin/bash $(workspace_flag) claude
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
start [args] Build images, start proxy, launch Claude Code
|
||||||
|
run [args] Start proxy if needed, launch Claude Code
|
||||||
|
stop Stop and remove all containers
|
||||||
|
update Rebuild images without cache
|
||||||
|
logs [svc] Tail logs (default: proxy)
|
||||||
|
status Show container status
|
||||||
|
shell Open a bash shell in the Claude container (debug)
|
||||||
|
help Show this message
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
ANTHROPIC_API_KEY Required. Set in .env or exported in your shell.
|
||||||
|
WORKSPACE_DIR Optional. Absolute path to mount as /workspace.
|
||||||
|
Defaults to a named Docker volume (fully isolated).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./claude.sh start
|
||||||
|
WORKSPACE_DIR=\$HOME/myproject ./claude.sh run
|
||||||
|
./claude.sh logs proxy
|
||||||
|
./claude.sh shell
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
||||||
|
case "${1:-help}" in
|
||||||
|
start) shift; cmd_start "$@" ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
run) shift; cmd_run "$@" ;;
|
||||||
|
update) cmd_update ;;
|
||||||
|
logs) shift; cmd_logs "${1:-}" ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
shell) cmd_shell ;;
|
||||||
|
help|-h|--help) cmd_help ;;
|
||||||
|
*)
|
||||||
|
error "Unknown command: ${1}"
|
||||||
|
cmd_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
59
docker-compose.yml
Normal file
59
docker-compose.yml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ─── Proxy sidecar ─────────────────────────────────────────────────────────
|
||||||
|
# Bridges the isolated internal network to the internet.
|
||||||
|
# Enforces an egress allowlist — see proxy/squid.conf.
|
||||||
|
proxy:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.proxy
|
||||||
|
networks:
|
||||||
|
- claude-internal # reachable by the claude container
|
||||||
|
- proxy-external # has outbound internet access
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /var/spool/squid
|
||||||
|
- /var/log/squid
|
||||||
|
|
||||||
|
# ─── Claude Code container ─────────────────────────────────────────────────
|
||||||
|
# No direct internet access. All egress routes through the proxy sidecar.
|
||||||
|
# Run via "docker compose run --rm claude" (managed by claude.sh).
|
||||||
|
claude:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.claude
|
||||||
|
depends_on:
|
||||||
|
proxy:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- claude-internal # only — no route to the internet
|
||||||
|
environment:
|
||||||
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||||
|
- HTTP_PROXY=http://proxy:3128
|
||||||
|
- HTTPS_PROXY=http://proxy:3128
|
||||||
|
- ALL_PROXY=http://proxy:3128
|
||||||
|
- NO_PROXY=localhost,127.0.0.1
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
# Workspace is injected by claude.sh via --volume flag at run time.
|
||||||
|
# Default: named Docker volume. Override: set WORKSPACE_DIR on the host.
|
||||||
|
|
||||||
|
networks:
|
||||||
|
# Internal-only: Docker adds no default gateway → no direct internet route
|
||||||
|
claude-internal:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
# External: standard bridge with internet access (proxy only)
|
||||||
|
proxy-external:
|
||||||
|
driver: bridge
|
||||||
44
proxy/squid.conf
Normal file
44
proxy/squid.conf
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Squid forward-proxy sidecar — allowlist-only egress for Claude Code
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
http_port 3128
|
||||||
|
|
||||||
|
# PID must be writable by the non-root proxy user
|
||||||
|
pid_filename /tmp/squid.pid
|
||||||
|
|
||||||
|
# ─── Logging (container-friendly: stdout/stderr) ──────────────────────────────
|
||||||
|
access_log stdio:/dev/stdout combined
|
||||||
|
cache_log stdio:/dev/stderr
|
||||||
|
cache_store_log none
|
||||||
|
|
||||||
|
# ─── No disk cache ────────────────────────────────────────────────────────────
|
||||||
|
cache deny all
|
||||||
|
coredump_dir /var/spool/squid
|
||||||
|
|
||||||
|
# ─── ACL Definitions ──────────────────────────────────────────────────────────
|
||||||
|
acl SSL_ports port 443
|
||||||
|
acl Safe_ports port 80
|
||||||
|
acl Safe_ports port 443
|
||||||
|
acl CONNECT method CONNECT
|
||||||
|
|
||||||
|
# ─── Egress allowlist ─────────────────────────────────────────────────────────
|
||||||
|
# Add domains here as needed. Leading dot matches all subdomains.
|
||||||
|
acl allowed_sites dstdomain api.anthropic.com
|
||||||
|
acl allowed_sites dstdomain statsig.anthropic.com
|
||||||
|
|
||||||
|
# ─── Access rules ─────────────────────────────────────────────────────────────
|
||||||
|
# Block requests to non-standard ports
|
||||||
|
http_access deny !Safe_ports
|
||||||
|
|
||||||
|
# Block CONNECT to non-SSL ports
|
||||||
|
http_access deny CONNECT !SSL_ports
|
||||||
|
|
||||||
|
# Allow HTTPS tunnels only to allowlisted destinations
|
||||||
|
http_access allow CONNECT allowed_sites
|
||||||
|
|
||||||
|
# Allow plain HTTP only to allowlisted destinations
|
||||||
|
http_access allow allowed_sites
|
||||||
|
|
||||||
|
# Deny everything else — default deny
|
||||||
|
http_access deny all
|
||||||
Loading…
Add table
Add a link
Reference in a new issue