Skip to main content

Container Isolation

Crewship uses multiple layers of isolation to prevent agents from accessing unauthorized resources, exfiltrating data, or interfering with other agents.

Isolation Layers

+-----------------------------------------------------------+
| Layer 5: Keeper (AI-powered credential gating)            |
+-----------------------------------------------------------+
| Layer 4: Network Policy (free/restricted domain filtering)|
+-----------------------------------------------------------+
| Layer 3: Sidecar Proxy (credential injection, scrubbing)  |
+-----------------------------------------------------------+
| Layer 2: UID Boundary (agent=1001, sidecar=1002)          |
+-----------------------------------------------------------+
| Layer 1: Container Runtime (Docker, gVisor, Kata, Sysbox) |
+-----------------------------------------------------------+

UID Security Boundary

Two fixed UIDs enforce a privilege boundary inside each container:
UIDProcessAccess
1001Agent process (Claude Code, etc.)Code execution, file I/O, HTTP via proxy
1002Sidecar proxyCredential store, IPC token, network control
The UID assignments (1001 for agent, 1002 for sidecar) are a security boundary. Do not change these values — they are referenced throughout the codebase for privilege separation.

What Agents Cannot Do

Because agents run as UID 1001 and the sidecar runs as UID 1002:
  • Agents cannot read the sidecar’s in-memory credential store
  • Agents cannot access the IPC token (used for crewshipd authentication)
  • Agents cannot modify network policy configuration
  • Agents cannot tamper with outbound request headers (the sidecar handles this)
  • Agents cannot set their own container ID in Keeper execute requests

Container Runtime Security

Crewship supports multiple container runtimes with different security profiles:
RuntimeSecurity LevelDescription
runcStandardDefault Docker runtime
runsc (gVisor)HighKernel-level syscall interception
kata-runtimeVery HighLightweight VM isolation
sysbox-runcHighEnhanced container isolation
Configure via:
container:
  default_runtime: "runsc"  # or "runc", "kata-runtime", "sysbox-runc"

Container Creation Security

Containers are created with these security measures (internal/provider/docker/docker.go):
  • Cap-drop ALL: All Linux capabilities are dropped
  • Cap-add NET_RAW: Re-added after dropping ALL (allows ICMP/ping for network diagnostics)
  • SecurityOpt no-new-privileges: Blocks execution of setuid binaries inside the container
  • Non-root execution: Agent processes run as UID 1001
  • Read-only filesystem: System directories are read-only (ReadonlyRootfs: true)
  • tmpfs /tmp (500M): Writable temp directory, not persistent across restarts
  • PID limit: 200: Prevents fork bomb attacks (PidsLimit: 200)
  • Memory limits: Configurable per-crew (default_memory_mb, default 512 MB)
  • CPU limits: Configurable per-crew (default_cpus, default 1.0)
  • Network isolation: Containers connect to a dedicated Docker bridge network
  • ExtraHosts: host.docker.internal:host-gateway enables container-to-host communication
  • Named volumes: crewship-home-{slug} and crewship-tools-{slug} for persistent home and tools directories

Network Policy

Each crew can have a network access policy:
All outbound connections are allowed. The sidecar still intercepts and injects credentials for known LLM providers, but does not block unknown domains.
# No explicit network policy = free mode
Unknown network modes default to restricted (fail closed). This prevents a typo in the configuration from accidentally allowing unrestricted access.

Domain Allowlist Implementation

The DomainAllowlist (internal/sidecar/allowlist.go) is a thread-safe set of allowed domain names:
  • Domains are stored lowercase for case-insensitive matching
  • Port numbers are stripped before comparison
  • IPv6 bracket notation is handled correctly
  • Domains can be added at runtime via allowlist.Add(domain)

Proxy Security

The sidecar proxy (internal/sidecar/proxy.go) enforces several security measures:

Request Body Limits

const maxRequestBodyBytes = 10 * 1024 * 1024 // 10 MB
All proxied requests have their body limited to 10 MB to prevent OOM attacks. LLM API requests are typically under 1 MB.

Hop-by-Hop Header Stripping

Per RFC 2616 Section 13.5.1, the proxy strips these headers:
HeaderRisk
ConnectionConnection control
Keep-AliveConnection control
Proxy-AuthenticateProxy auth
Proxy-AuthorizationData exfiltration vector
TETransfer encoding
TrailersTransfer encoding
Transfer-EncodingTransfer encoding
UpgradeProtocol upgrade
Proxy-Authorization is especially dangerous — an agent could use it to exfiltrate credentials via a controlled proxy.

HTTPS CONNECT Tunnels

For HTTPS requests, the sidecar:
  1. Checks the domain allowlist (in restricted mode)
  2. Establishes a TCP tunnel
  3. Does not inject credentials (the tunnel is opaque)
Credential injection only works for:
  • Plain HTTP proxy requests (agent sets HTTP_PROXY)
  • Reverse proxy mode (ANTHROPIC_BASE_URL=http://127.0.0.1:9119)

Provider Detection

The sidecar automatically detects which LLM provider a request is targeting:
func providerForHost(host string) ProviderType {
    switch host {
    case "api.anthropic.com":    return ProviderAnthropic
    case "api.openai.com":       return ProviderOpenAI
    case "generativelanguage.googleapis.com": return ProviderGoogle
    default:                     return ""  // no injection
    }
}

Credential Injection by Provider

ProviderAuth Method
Anthropic (API key)x-api-key: {key} + anthropic-version: 2023-06-01
Anthropic (OAuth)Authorization: Bearer {token} (detected by sk-ant-oat prefix)
OpenAIAuthorization: Bearer {key}
Google?key={key} query parameter

Keeper Execute Security

The /keeper/execute flow has the most stringent security because it runs shell commands with credentials:
  1. Sidecar validation:
    • Intent and command length limits (4096 chars)
    • Null byte rejection
    • Shell metacharacter blocking (;, |, `, >, &&, ||, $()
    • Content inside single quotes is exempt
    • Container ID always from IPC config (agent cannot override)
  2. crewshipd validation:
    • Same shell metacharacter checks (defense in depth)
    • Keeper LLM evaluates the command
  3. Execution:
    • Credential injected as environment variable for the command only
    • Output scrubbed of credential values before returning to agent

Output Scrubbing

The scrubber package (internal/scrubber/) removes credential values from agent output. This prevents:
  • Credentials appearing in chat messages
  • Credentials being logged to the progress stream
  • Credentials in Keeper execute output being returned to the agent

Container Auto-Detection

The Docker provider (internal/provider/docker/docker.go) auto-detects the container runtime by probing socket paths:
Socket PathRuntime
/var/run/docker.sockDocker
~/.colima/default/docker.sockColima
~/.orbstack/run/docker.sockOrbStack
~/.rd/docker.sockRancher Desktop
~/.docker/run/docker.sockDocker Desktop (macOS)
/run/user/{uid}/podman/podman.sockPodman (rootless)
/run/podman/podman.sockPodman (root)
/run/containerd/containerd.socknerdctl
Each socket gets a 1.5-second ping timeout to avoid blocking on unresponsive daemons.