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:
readcan inspect state through read-only routes.execcan read state and operate sandbox/envd surfaces.admincan do everything, including templates, pools, schedules, secrets, volumes, webhooks, limits, and network policy.
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:
/healthis public./sandboxesrejects no key and a bad key with 401./sandboxesacceptsX-API-Key./auth/whoamiacceptsAuthorization: Bearerand returns the matching tenant.- A
read-only key can list sandboxes/templates but gets 403 for sandbox creation, template mutation, and envd execution. - envd
/contextsis protected by the same middleware. - Route tests cover tenant quota parsing and prove the authenticated tenant marker overrides caller/backend metadata.
/audit/eventsrequiresadminscope and returns the denied decisions generated during the smoke.
Latest receipt: benchmarks/results/api-key-auth/latest.txt.