Ctrl K
ComunidadGuías…

sandbox-service/docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Server deployment for the Aivory sandbox.
#
# Pull-and-run on the server (no build needed — images come from ghcr.io):
#   export OWNER=hjxwz123
#   export SANDBOX_API_KEY=$(openssl rand -hex 24)
#   docker compose pull && docker compose up -d
#
# Then point the Go backend at it:
#   SANDBOX_BASE_URL=http://<server>:48217
#   SANDBOX_API_KEY=<same value as above>
#
# The sidecar drives the host Docker daemon (mounted socket) to spawn one
# locked-down `aivory-sandbox` container per session. SANDBOX_PULL_ON_START
# makes it fetch the runtime image on boot so the first /sessions call works.
#
# ─────────────────────────────────────────────────────────────────────────────
# SECURITY NOTE — host-takeover risk via docker.sock + root sidecar (F1)
# ─────────────────────────────────────────────────────────────────────────────
# This service mounts /var/run/docker.sock and uses it to create/start/exec
# containers. That socket is root-equivalent on the host: anyone who can drive
# it (i.e. anyone who reaches this service) can take over the host (mount the
# host FS into a container, run privileged, etc.). The architecture REQUIRES the
# socket, so it is intentionally kept — but harden the blast radius:
#
#   1. SANDBOX_API_KEY is MANDATORY. The sidecar now refuses to start without it
#      (set SANDBOX_ALLOW_NO_AUTH=1 only on a trusted localhost-only dev box).
#   2. Do NOT expose port 48217 to the public internet. Bind it to a private
#      interface / VPN, or front it with mTLS. The main app reaches it over the
#      private network only.
#   3. STRONGLY RECOMMENDED: put a docker-socket-proxy in front of the real
#      socket instead of mounting it raw, restricted to just the calls this
#      service makes (container create/start/exec/kill/inspect + image pull):
#
#        socket-proxy:
#          image: tecnativa/docker-socket-proxy
#          environment:
#            CONTAINERS: 1    # create / inspect / rm
#            POST: 1          # required for create/start/exec/kill
#            EXEC: 1          # docker exec
#            IMAGES: 1        # docker pull (SANDBOX_PULL_ON_START)
#            VERSION: 1       # /healthz
#            # leave everything else at 0 (no SWARM/NETWORKS/VOLUMES/INFO/AUTH…)
#          volumes:
#            - /var/run/docker.sock:/var/run/docker.sock:ro
#          # do not publish this; only the sandbox service should reach it
#        # then on the sandbox service set: DOCKER_HOST: tcp://socket-proxy:2375
#        # and mount nothing under /var/run.
#
#   4. Pin a seccomp profile on the SESSION containers via SANDBOX_SECCOMP_PROFILE
#      (path readable INSIDE this sidecar; e.g. mount a profile and point at it).
#      Combined with the already-set --cap-drop ALL and
#      --security-opt no-new-privileges this narrows the per-session kernel
#      attack surface. Leave unset to use docker's built-in default profile.
# ─────────────────────────────────────────────────────────────────────────────

services:
  sandbox:
    image: ghcr.io/${OWNER:-hjxwz123}/aivory-sandbox-sidecar:latest
    restart: unless-stopped
    ports:
      # Host port is an uncommon high port to avoid clashes with other
      # services; the in-container port stays 8000.
      - "48217:8000"
    environment:
      SANDBOX_IMAGE: ghcr.io/${OWNER:-hjxwz123}/aivory-sandbox:latest
      SANDBOX_PULL_ON_START: "1"
      SANDBOX_MEMORY: 2g
      SANDBOX_CPUS: "1"
      SANDBOX_MAX_SESSIONS: "16"
      SANDBOX_MAX_CONCURRENT_EXECS: "4"
      SANDBOX_MAX_CONCURRENT_CREATES: "2"
      SANDBOX_MAX_UPLOAD_BYTES: "41943040"
      SANDBOX_MAX_FILES_PER_EXEC: "20"
      SANDBOX_MAX_TOTAL_ARTIFACT_BYTES: "52428800"
      # F5: control-plane OOM guards (reject oversized requests up front).
      SANDBOX_MAX_BODY_BYTES: "58720256"   # 56 MiB; > 40 MiB upload after base64
      SANDBOX_MAX_CODE_BYTES: "1048576"    # 1 MiB cap on /exec `code`
      # F6: disk-fill DoS guards. Read-only rootfs + size-bounded writable
      # /workspace tmpfs are ON by default; tune the workspace size here. Set
      # SANDBOX_DISK_SIZE only on an overlay2+prjquota host (opt-in, best-effort).
      SANDBOX_READ_ONLY_ROOTFS: "1"
      SANDBOX_WORKSPACE_SIZE: 512m
      # SANDBOX_DISK_SIZE: 1g
      # §4.5-F zero-config workspace persistence: the admin `storage_provider`
      # defaults to "local", so reaped /workspace tarballs are archived here (the
      # named volume below) and RESTORED when the conversation runs code again —
      # archives are keyed by conversation id, so they survive session recycle
      # (§4.5-C G2). No S3/OSS/MinIO needed. Unset the provider (or drop the
      # volume) for ephemeral "reaped = gone". Single-node only (a plain volume
      # isn't shared across replicas); multi-replica deployments must use S3/OSS.
      SANDBOX_LOCAL_STORAGE_DIR: /var/lib/aivory/sandbox-archives
      # F1: optional pinned seccomp profile for session containers (path must be
      # readable INSIDE this sidecar container — mount it and point here).
      # SANDBOX_SECCOMP_PROFILE: /seccomp/aivory-sandbox.json
      # F2: MANDATORY. The sidecar exposes host Docker control (RCE) and refuses
      # to start without this. Generate with: openssl rand -hex 24
      SANDBOX_API_KEY: ${SANDBOX_API_KEY:?set SANDBOX_API_KEY (openssl rand -hex 24); the sandbox refuses to start without it}
    # Liveness probe via urllib (no curl/wget in the image); non-2xx raises ->
    # unhealthy. start_period covers SANDBOX_PULL_ON_START on a cold boot.
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz', timeout=5)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 120s
    # Bound the control-plane container itself; mem_limit must exceed the in-memory
    # archive buffer (SANDBOX_MAX_ARCHIVE_BYTES, default 200 MiB) + storage bodies.
    mem_limit: 1g
    pids_limit: 512
    volumes:
      # The sidecar needs the host daemon to spawn sibling session containers.
      # See the SECURITY NOTE above: prefer a docker-socket-proxy over the raw
      # socket, restricted to container create/start/exec/kill + image pull.
      - /var/run/docker.sock:/var/run/docker.sock
      # §4.5 durable local archive store (SANDBOX_LOCAL_STORAGE_DIR above). Keep
      # it OUT of any session container's reach — it holds cross-session tarballs.
      - sandbox-archives:/var/lib/aivory/sandbox-archives

  # Pins the runtime image locally so `docker image prune -a` (or a cleanup cron)
  # can't evict it between sessions — the per-session containers are ephemeral, so
  # otherwise nothing references it and the next session-create cold-pulls ~600MB
  # and times out (500). This idle container just sleeps at ~zero cost. It carries
  # NO `aivory.sandbox=1` label and no `aivory-sbx-` name, so the sidecar's reaper
  # / MAX_SESSIONS counter ignore it.
  sandbox-image-keepalive:
    image: ghcr.io/${OWNER:-hjxwz123}/aivory-sandbox:latest
    command: ["sleep", "infinity"]
    restart: unless-stopped
    network_mode: none
    mem_limit: 64m
    pids_limit: 16

volumes:
  sandbox-archives: