From fe0f560b589aa9f57f658b2a57bb2a9b3ee609c5 Mon Sep 17 00:00:00 2001 From: Timmy Date: Sat, 21 Mar 2026 20:33:50 +0000 Subject: [PATCH] Harden Docker container security Run as non-root user (fixes Claude Code refusing bypassPermissions as root, which caused all agent spawns to exit instantly with no session). Add read-only root filesystem, drop all capabilities, set no-new-privileges, bind port to localhost only, and require GIT_USER_NAME/GIT_USER_EMAIL env vars at startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/Dockerfile | 30 ++++++++++++++++++++---------- docker/docker-compose.yml | 26 +++++++++++++++++++++++--- docker/entrypoint.sh | 24 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100755 docker/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index cb772e9..7149e5e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,10 +33,6 @@ RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/bin # The CLI binary is `claude`. RUN npm install -g @anthropic-ai/claude-code -# ── Biome (frontend linter) ───────────────────────────────────────── -# Installed project-locally via npm install, but having it global avoids -# needing node_modules for CI-style checks. - # ── Working directory ──────────────────────────────────────────────── # /app holds the storkit source (copied in at build time for the binary). # /workspace is where the target project repo gets bind-mounted at runtime. @@ -98,6 +94,22 @@ COPY --from=base /usr/local/bin/storkit /usr/local/bin/storkit # Alternative: mount the source as a volume. COPY --from=base /app /app +# ── Non-root user ──────────────────────────────────────────────────── +# Claude Code refuses --dangerously-skip-permissions (bypassPermissions) +# when running as root. Create a dedicated user so agents can launch. +RUN groupadd -r storkit \ + && useradd -r -g storkit -m -d /home/storkit storkit \ + && mkdir -p /home/storkit/.claude \ + && chown -R storkit:storkit /home/storkit \ + && chown -R storkit:storkit /usr/local/cargo /usr/local/rustup \ + && chown -R storkit:storkit /app + +# ── Entrypoint ─────────────────────────────────────────────────────── +# Validates required env vars (GIT_USER_NAME, GIT_USER_EMAIL) and +# configures git identity before starting the server. +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh + +USER storkit WORKDIR /workspace # ── Ports ──────────────────────────────────────────────────────────── @@ -105,11 +117,9 @@ WORKDIR /workspace EXPOSE 3001 # ── Volumes (defined in docker-compose.yml) ────────────────────────── -# /workspace – bind mount: target project repo -# /root/.claude – named volume: Claude Code sessions/state -# /usr/local/cargo/registry – named volume: cargo dependency cache +# /workspace – bind mount: target project repo +# /home/storkit/.claude – named volume: Claude Code sessions/state +# /usr/local/cargo/registry – named volume: cargo dependency cache -# ── Entrypoint ─────────────────────────────────────────────────────── -# Run storkit against the bind-mounted project at /workspace. -# The server picks up ANTHROPIC_API_KEY from the environment. +ENTRYPOINT ["entrypoint.sh"] CMD ["storkit", "/workspace"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7dce0b7..4200c7b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,11 +16,14 @@ services: dockerfile: docker/Dockerfile container_name: storkit ports: - # Web UI + MCP endpoint - - "3001:3001" + # Bind to localhost only — not exposed on all interfaces. + - "127.0.0.1:3001:3001" environment: # Required: Anthropic API key for Claude Code agents - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY} + # Required: git identity for agent commits + - GIT_USER_NAME=${GIT_USER_NAME:?Set GIT_USER_NAME} + - GIT_USER_EMAIL=${GIT_USER_EMAIL:?Set GIT_USER_EMAIL} # Optional: override the server port (default 3001) - STORKIT_PORT=3001 # Optional: Matrix bot credentials (if using Matrix integration) @@ -45,7 +48,7 @@ services: # Claude Code state – persists session history, projects config, # and conversation transcripts so --resume works across restarts. - - claude-state:/root/.claude + - claude-state:/home/storkit/.claude # Storkit source tree for rebuild_and_restart. # The binary has CARGO_MANIFEST_DIR baked in at compile time @@ -63,6 +66,23 @@ services: - workspace-target:/workspace/target - storkit-target:/app/target + # ── Security hardening ────────────────────────────────────────── + # Read-only root filesystem. Only explicitly mounted volumes and + # tmpfs paths are writable. + read_only: true + tmpfs: + - /tmp:size=512M + - /home/storkit/.npm:size=256M + + # Drop all Linux capabilities, then add back only what's needed. + cap_drop: + - ALL + + # Prevent child processes from gaining new privileges via setuid, + # setgid, or other mechanisms. + security_opt: + - no-new-privileges:true + # Resource limits – cap the whole system. # Adjust based on your machine. These are conservative defaults. deploy: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..6cb0e93 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e + +# ── Git identity ───────────────────────────────────────────────────── +# Agents commit code inside the container. Without a git identity, +# commits fail or use garbage defaults. Fail loudly at startup so the +# operator knows immediately. +if [ -z "$GIT_USER_NAME" ]; then + echo "FATAL: GIT_USER_NAME is not set. Export it in your environment or docker-compose.yml." >&2 + exit 1 +fi +if [ -z "$GIT_USER_EMAIL" ]; then + echo "FATAL: GIT_USER_EMAIL is not set. Export it in your environment or docker-compose.yml." >&2 + exit 1 +fi + +# Use GIT_AUTHOR/COMMITTER env vars instead of git config --global, +# so the root filesystem can stay read-only (no ~/.gitconfig write). +export GIT_AUTHOR_NAME="$GIT_USER_NAME" +export GIT_COMMITTER_NAME="$GIT_USER_NAME" +export GIT_AUTHOR_EMAIL="$GIT_USER_EMAIL" +export GIT_COMMITTER_EMAIL="$GIT_USER_EMAIL" + +exec "$@"