fix(docker): restore base capabilities in sandbox overlay (#523)

Sandbox overlay's cap_add replaces (not merges) the base compose, dropping
SETUID, SETGID, CHOWN. This causes credential copy to fail with Permission
denied when combining sandbox + claude-cli overlays.

Changes:
- Re-include base capabilities in sandbox overlay's cap_add
- Use umask 077 for atomic permission-safe credential copy
- Add ENABLE_CLAUDE_CLI build arg to pre-install Claude CLI in image
- Add runtime warning when credentials mounted but CLI binary missing
- Add WITH_CLAUDE_CLI to Makefile for overlay consistency
- Add security warning comment for sandbox overlay attack surface
This commit is contained in:
therichardngai-code authored and GitHub committed 2026-03-28 13:17:08 +07:00
1 parent 0cfe5800ca
commit 6bfad07ed8
6 files changed
+38 -6

No files matched your search

+4
View File
@@ -13,5 +13,9 @@ POSTGRES_PASSWORD=
# Docker Compose auto-builds this from POSTGRES_USER/PASSWORD/DB.
# GOCLAW_POSTGRES_DSN=postgres://user:pass@host:5432/dbname?sslmode=disable
# --- Sandbox (only when using docker-compose.sandbox.yml) ---
# Docker socket GID: 999 on Linux, 0 on Windows/macOS Docker Desktop.
# DOCKER_GID=0
# --- Debug ---
# GOCLAW_TRACE_VERBOSE=1
+6 -1
View File
@@ -45,6 +45,7 @@ ARG ENABLE_SANDBOX=false
ARG ENABLE_PYTHON=false
ARG ENABLE_NODE=false
ARG ENABLE_FULL_SKILLS=false
ARG ENABLE_CLAUDE_CLI=false
# Install ca-certificates + wget (healthcheck) + optional runtimes.
# ENABLE_FULL_SKILLS=true pre-installs all skill deps (larger image, no on-demand install needed).
@@ -66,9 +67,13 @@ RUN set -eux; \
apk add --no-cache python3 py3-pip; \
pip3 install --no-cache-dir --break-system-packages edge-tts; \
fi; \
if [ "$ENABLE_NODE" = "true" ]; then \
if [ "$ENABLE_NODE" = "true" ] || [ "$ENABLE_CLAUDE_CLI" = "true" ]; then \
apk add --no-cache nodejs npm; \
fi; \
fi; \
if [ "$ENABLE_CLAUDE_CLI" = "true" ]; then \
npm install -g --cache /tmp/npm-cache @anthropic-ai/claude-code; \
rm -rf /tmp/npm-cache; \
fi
# Non-root user
+3
View File
@@ -33,6 +33,9 @@ endif
ifdef WITH_REDIS
COMPOSE_EXTRA += -f docker-compose.redis.yml
endif
ifdef WITH_CLAUDE_CLI
COMPOSE_EXTRA += -f docker-compose.claude-cli.yml
endif
COMPOSE = $(COMPOSE_BASE) $(COMPOSE_EXTRA)
UPGRADE = docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.upgrade.yml
+6 -2
View File
@@ -1,10 +1,14 @@
# Optional overlay: sync Claude CLI credentials from host into container.
# Optional overlay: install Claude CLI and sync credentials from host into container.
# Mounts host ~/.claude as read-only; entrypoint copies credentials to the data volume.
# Adds ENABLE_CLAUDE_CLI build arg to install nodejs + @anthropic-ai/claude-code.
#
# Usage:
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.claude-cli.yml up -d
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.claude-cli.yml up -d --build
services:
goclaw:
build:
args:
ENABLE_CLAUDE_CLI: "true"
volumes:
- ${HOME}/.claude:/app/.claude-host:ro
+7 -1
View File
@@ -26,10 +26,16 @@ services:
- GOCLAW_SANDBOX_CPUS=1.0
- GOCLAW_SANDBOX_TIMEOUT_SEC=300
- GOCLAW_SANDBOX_NETWORK=false
# Override base cap_drop to allow Docker socket access
# Override base cap_drop to allow Docker socket access.
# Re-include base caps (SETUID/SETGID/CHOWN) lost when overriding cap_add.
# WARNING: SETUID/SETGID with security_opt cleared (no no-new-privileges)
# increases attack surface. Only use in trusted environments.
cap_drop: []
cap_add:
- NET_BIND_SERVICE
- SETUID
- SETGID
- CHOWN
security_opt: []
group_add:
- ${DOCKER_GID:-999}
+12 -2
View File
@@ -74,13 +74,23 @@ fi
# Copy Claude CLI credentials from root-owned read-only mount to goclaw-accessible location.
# /app/.claude is a symlink → /app/data/.claude (writable volume, see Dockerfile).
# Uses install(1) for atomic copy with correct ownership+permissions (no temp file needed).
# Uses su-exec to copy as goclaw user because sandbox overlay's cap_add override
# may remove CHOWN needed by install(1). umask 077 ensures file is created with 600.
if [ -f /app/.claude-host/.credentials.json ]; then
(mkdir -p /app/data/.claude \
&& install -m 600 -o goclaw -g goclaw /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json \
&& if command -v su-exec >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then
su-exec goclaw sh -c 'umask 077 && cp /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json'
else
( umask 077 && cp /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json )
fi \
&& echo "Claude CLI credentials synced from host.") || echo "WARNING: Claude credentials copy failed (non-fatal)"
fi
# Warn if Claude credentials are mounted but CLI binary is missing (forgot --build).
if [ -d /app/.claude-host ] && ! command -v claude >/dev/null 2>&1; then
echo "WARNING: Claude credentials mounted but claude CLI not installed. Rebuild with: --build"
fi
# Run command with privilege drop (su-exec in Docker, direct otherwise).
run_as_goclaw() {
if command -v su-exec >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then