#!/usr/bin/env bash set -uo pipefail # CASAN Track C — TRUE runtime isolation via container (C6 / V22, production form). # # Upgrades the static-policy scaffold (sandbox-run.sh) to real kernel isolation: # the command runs inside a locked-down container where the KERNEL — not a grep — # neutralises escapes: # --network=none → no egress at all # --read-only → root filesystem is immutable (can't write outside workspace) # --pids-limit → fork bombs are capped # --memory/--cpus → resource abuse is bounded # -v :/work:rw → ONLY the workspace is writable; host $HOME/.ssh is NOT mounted # --cap-drop=ALL --security-opt=no-new-privileges → no privilege escalation # --user=65532:65532 → workload process is never root in the container # # Usage: # sandbox-container.sh --workspace [--image busybox] [--timeout 20] # [--memory 256m] [--pids 128] [--cpus 1] -- # Exit: command's exit code · 124 timeout · 2 policy/setup error · 127 no docker. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${CASAN_SANDBOX_IMAGE:-busybox}" WORKSPACE="$PWD"; TIMEOUT="${CASAN_SANDBOX_TIMEOUT:-20}" MEMORY="${CASAN_SANDBOX_MEMORY:-256m}"; PIDS="${CASAN_SANDBOX_PIDS:-128}"; CPUS="${CASAN_SANDBOX_CPUS:-1}" USER_SPEC="${CASAN_SANDBOX_USER:-65532:65532}" while [[ "$#" -gt 0 ]]; do case "$1" in --workspace) WORKSPACE="${2:-}"; shift 2 ;; --image) IMAGE="${2:-}"; shift 2 ;; --timeout) TIMEOUT="${2:-}"; shift 2 ;; --memory) MEMORY="${2:-}"; shift 2 ;; --pids) PIDS="${2:-}"; shift 2 ;; --cpus) CPUS="${2:-}"; shift 2 ;; --user) USER_SPEC="${2:-}"; shift 2 ;; --) shift; break ;; *) echo "sandbox-container: unknown arg $1" >&2; exit 2 ;; esac done [[ "$#" -ge 1 ]] || { echo "Usage: sandbox-container.sh --workspace -- " >&2; exit 2; } command -v docker >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_NO_DOCKER" >&2; exit 127; } docker info >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_DOCKER_DOWN" >&2; exit 127; } [[ "$USER_SPEC" =~ ^[0-9]+:[0-9]+$ ]] || { echo "SANDBOX_CONTAINER_BAD_USER" >&2; exit 2; } # A non-root process is always required. Production additionally refuses a # rootful Docker daemon because a compromised daemon socket defeats container # isolation. Local developer/test profiles may use a rootful daemon, but cannot # claim that configuration as a hardened production runner. if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \ || "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'rootless' \ || { echo "SANDBOX_CONTAINER_ROOTLESS_REQUIRED" >&2; exit 2; } fi if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then [[ "$IMAGE" =~ @sha256:[a-f0-9]{64}$ ]] \ || { echo "SANDBOX_CONTAINER_IMAGE_DIGEST_REQUIRED image=$IMAGE" >&2; exit 2; } fi WS_ABS="$(cd "$WORKSPACE" 2>/dev/null && pwd)" || { echo "SANDBOX_CONTAINER_BAD_WORKSPACE" >&2; exit 2; } # Join the command into a single shell string to run inside the container. CMD="$*" # Hardened container. --init reaps zombies; tmpfs gives a small writable /tmp # without a writable rootfs. The wall-clock timeout goes through tool-exec.sh # (portable: `timeout` if present, else a perl alarm — macOS has no coreutils # `timeout`). Container name is tracked so a timed-out container is force-removed. CID="casan-sbx-$$-${RANDOM}" set +e bash "$SCRIPT_DIR/tool-exec.sh" "$TIMEOUT" -- \ docker run --rm --init --name "$CID" \ --network=none --read-only \ --pids-limit="$PIDS" --memory="$MEMORY" --cpus="$CPUS" \ --user "$USER_SPEC" --cap-drop=ALL --security-opt=no-new-privileges \ --tmpfs /tmp:rw,size=16m \ -v "$WS_ABS":/work:rw -w /work \ "$IMAGE" sh -c "$CMD" rc=$? set -e if [[ "$rc" -eq 124 ]]; then docker rm -f "$CID" >/dev/null 2>&1 || true echo "SANDBOX_CONTAINER_TIMEOUT after ${TIMEOUT}s" >&2 exit 124 fi exit "$rc"