API key authentication

Coppice stays unauthenticated by default because the demo deployment is normally bound to localhost and reached through an SSH tunnel. When COPPICE_API_KEYS or COPPICE_API_KEYS_FILE is configured, the gateway requires credentials on the API and envd surfaces while leaving /health, /ui/, and already-signed download URLs reachable.

Configure

Inline keys are enough for single-node deployments:

export COPPICE_API_KEYS='team-a:sk-team-a:admin|exec,team-b:sk-team-b:read'
service e2b-compat restart

The file form accepts either the same line format or JSON:

[
  {"tenantID":"team-a","name":"ci","key":"sk-team-a","scopes":["admin","exec"]},
  {"tenantID":"team-b","name":"reader","key":"sk-team-b","scopes":["read"]}
]

Clients can send either shape:

curl -H 'X-API-Key: sk-team-a' http://127.0.0.1:3000/sandboxes
curl -H 'Authorization: Bearer sk-team-a' http://127.0.0.1:3000/auth/whoami

The coppice CLI now reads —token, COPPICE_TOKEN, or E2B_API_KEY and sends a Bearer token automatically. Existing E2B SDK flows already carry E2B_API_KEY.

Scope

This is authentication, tenant labelling, and coarse route-level scope enforcement. The gateway records the key’s tenantID and scopes in /auth/whoami, rejects missing or invalid keys before a route runs, and then applies three built-in scope classes:

When a sandbox is created through an authenticated request, the gateway adds a reserved coppice_tenant_id metadata marker after caller and backend metadata are merged. That marker is used for active sandbox quota accounting and cannot be spoofed through the create payload.

Tenant quotas

Set a global active-sandbox cap for every authenticated tenant:

export COPPICE_TENANT_MAX_SANDBOXES=8

Override per tenant with comma- or newline-separated entries. Specific tenant values win; * is the fallback for tenants not named:

export COPPICE_TENANT_SANDBOX_LIMITS='team-a=2,team-b=10,*=4'

The cap counts running and paused sandboxes for the authenticated tenantID. A tenant at its cap receives 403 before the backend clone/start path runs. Unauthenticated local demo mode and internally scheduled runs are not quota-limited by this v1 path.

Auth and scope decisions also feed the control-plane audit log. The next hardening layer is durable org/key management and a UI for issuing/revoking keys.

That split is intentional. It lets a self-hosted operator put a real credential boundary on the gateway today without pretending a single FreeBSD host is already a multi-tenant SaaS control plane.

Receipt

benchmarks/rigs/api-key-auth-smoke.sh starts a local gateway with two keys and proves:

Latest receipt: benchmarks/results/api-key-auth/latest.txt.