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) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-03-21 20:33:50 +00:00
parent 0416bf343c
commit fe0f560b58
3 changed files with 67 additions and 13 deletions

View File

@@ -33,10 +33,6 @@ RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/bin
# The CLI binary is `claude`. # The CLI binary is `claude`.
RUN npm install -g @anthropic-ai/claude-code 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 ──────────────────────────────────────────────── # ── Working directory ────────────────────────────────────────────────
# /app holds the storkit source (copied in at build time for the binary). # /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. # /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. # Alternative: mount the source as a volume.
COPY --from=base /app /app 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 WORKDIR /workspace
# ── Ports ──────────────────────────────────────────────────────────── # ── Ports ────────────────────────────────────────────────────────────
@@ -106,10 +118,8 @@ EXPOSE 3001
# ── Volumes (defined in docker-compose.yml) ────────────────────────── # ── Volumes (defined in docker-compose.yml) ──────────────────────────
# /workspace bind mount: target project repo # /workspace bind mount: target project repo
# /root/.claude named volume: Claude Code sessions/state # /home/storkit/.claude named volume: Claude Code sessions/state
# /usr/local/cargo/registry named volume: cargo dependency cache # /usr/local/cargo/registry named volume: cargo dependency cache
# ── Entrypoint ─────────────────────────────────────────────────────── ENTRYPOINT ["entrypoint.sh"]
# Run storkit against the bind-mounted project at /workspace.
# The server picks up ANTHROPIC_API_KEY from the environment.
CMD ["storkit", "/workspace"] CMD ["storkit", "/workspace"]

View File

@@ -16,11 +16,14 @@ services:
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
container_name: storkit container_name: storkit
ports: ports:
# Web UI + MCP endpoint # Bind to localhost only — not exposed on all interfaces.
- "3001:3001" - "127.0.0.1:3001:3001"
environment: environment:
# Required: Anthropic API key for Claude Code agents # Required: Anthropic API key for Claude Code agents
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY} - 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) # Optional: override the server port (default 3001)
- STORKIT_PORT=3001 - STORKIT_PORT=3001
# Optional: Matrix bot credentials (if using Matrix integration) # Optional: Matrix bot credentials (if using Matrix integration)
@@ -45,7 +48,7 @@ services:
# Claude Code state persists session history, projects config, # Claude Code state persists session history, projects config,
# and conversation transcripts so --resume works across restarts. # and conversation transcripts so --resume works across restarts.
- claude-state:/root/.claude - claude-state:/home/storkit/.claude
# Storkit source tree for rebuild_and_restart. # Storkit source tree for rebuild_and_restart.
# The binary has CARGO_MANIFEST_DIR baked in at compile time # The binary has CARGO_MANIFEST_DIR baked in at compile time
@@ -63,6 +66,23 @@ services:
- workspace-target:/workspace/target - workspace-target:/workspace/target
- storkit-target:/app/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. # Resource limits cap the whole system.
# Adjust based on your machine. These are conservative defaults. # Adjust based on your machine. These are conservative defaults.
deploy: deploy:

24
docker/entrypoint.sh Executable file
View File

@@ -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 "$@"