Tokens & quotas¶
Firebox is multi-tenant from day one. Every HTTP call carries a bearer
token; the daemon resolves it to a Token record, then enforces
ownership + quota.
Token schema¶
Every scoped token is a JSON file at /etc/firebox/tokens.d/<id>.json:
{
"id": "alice",
"secret": "75d7b307a243c94be7a620fe3c5bf8101be3d9bf",
"admin": false,
"max_sandboxes": 5,
"max_mem_mib": 8192,
"max_ttl_seconds": 3600,
"note": "alice@example.com",
"created_at": 1745678901
}
| Field | Meaning |
|---|---|
id |
Unique label. Used in firebox token output, in 403 messages, in audit logs. |
secret |
The bearer string the client sends. Never echoed by the API. |
admin |
true bypasses every scope and quota check. |
max_sandboxes |
Hard limit on concurrent sandboxes owned by this token. 0 = unlimited. |
max_mem_mib |
Cumulative RAM cap across this token's live VMs. 0 = unlimited. |
max_ttl_seconds |
Largest ttl_seconds the client can ask for. 0 = unlimited. |
note |
Free text — typically the human owner. |
Admin CLI¶
Run from anywhere your FIREBOX_TOKEN is admin:
firebox token list
# ID ADMIN SANDBOXES MEM (MiB) TTL (s) NOTE
# legacy yes ∞ ∞ ∞ bootstrap admin
# alice no 5 8192 3600 alice@example.com
firebox token create alice \
--max-sandboxes 5 \
--max-mem-mib 8192 \
--max-ttl-seconds 3600 \
--note "alice@example.com"
# → prints the secret. **Copy it once and hand to the user.**
firebox token revoke alice
firebox token create auto-generates a 24-byte hex secret if you
don't pass --secret. The CLI is the only place that ever prints
secrets — they aren't echoed by firebox token list.
Auth flow¶
sequenceDiagram
autonumber
participant C as Client
participant D as Daemon
participant FS as Token files
C->>D: HTTP request<br/>Authorization: Bearer <secret>
D->>FS: read /etc/firebox/tokens.d/*.json + legacy file
D->>D: hmac.compare_digest(presented, stored)
alt no match
D-->>C: 401 Unauthorized
else admin
D-->>C: 200 (everything allowed)
else scoped
D->>D: scope + quota check
alt over quota
D-->>C: 429 Too Many Requests<br/>(precise reason)
else not owner of target
D-->>C: 403 Forbidden
else
D-->>C: 200
end
end
Tokens are read fresh on every request — adding / removing / editing files takes effect immediately, no daemon restart.
Quota enforcement at create time¶
When a non-admin token does Sandbox.create() the daemon runs
check_create_allowed() before anything is allocated:
flowchart TD
Req["create(template, ttl, mem)"]
Per["Sum the token's live sandboxes:<br/>(count, total_mem)"]
Glob["Sum globally across all tokens"]
SC{count + 1 ><br/>token.max_sandboxes?}
MM{total_mem + req.mem ><br/>token.max_mem_mib?}
TT{req.ttl ><br/>token.max_ttl_seconds?}
GS{global count ><br/>limits.max_total_sandboxes?}
GM{global mem + req.mem ><br/>limits.max_total_mem_mib?}
OK[Allocate sandbox]
F429[429 with reason]
Req --> Per --> Glob --> SC
SC -- yes --> F429
SC -- no --> MM
MM -- yes --> F429
MM -- no --> TT
TT -- yes --> F429
TT -- no --> GS
GS -- yes --> F429
GS -- no --> GM
GM -- yes --> F429
GM -- no --> OK
Failure messages are precise:
HTTP 429
{"error": "token 'alice' would exceed max_mem_mib (2048 > 1024)"}
HTTP 429
{"error": "token 'alice' requested ttl 600.0s exceeds max_ttl_seconds 120s"}
HTTP 429
{"error": "token 'alice' would exceed max_sandboxes (2 ≥ 2)"}
HTTP 429
{"error": "daemon at global cap max_total_sandboxes=60"}
Admin tokens skip the whole chain.
Visibility scoping¶
GET /sandboxes filters to what the calling token owns. Per-sandbox
routes (run, close, files/*, browser/*, ...) return 403 if
the sandbox's owner_token_id doesn't match. Admin tokens see / can
operate on everything.
Global limits¶
Optional /etc/firebox/limits.json:
Caps the daemon as a whole — useful when one runaway user shouldn't
exhaust the host. 0 (or missing field) means unlimited.
Backward compatibility¶
If /etc/firebox/tokens.d/ is empty and the legacy single-string
/etc/firebox/token exists, the daemon promotes that file to an
admin token with id legacy. Single-user deployments never had to
migrate.
The promoted legacy token stays valid even after you add scoped
tokens — so creating your first user token doesn't accidentally lock
out the bootstrap admin.
Day-to-day patterns¶
- Per-user tokens, no admin sharing. Issue a scoped token to each human / agent that calls the daemon. Reserve admin for the operator's own tooling.
- Tight
max_ttl_secondsfor unattended jobs that shouldn't be able to camp the host. - Burst-friendly
max_sandboxesbut tightmax_mem_mibso a curious user can experiment without taking 96 GB at once. - Revoke + reissue if a token leaks. The legacy admin always works to clean up in an emergency.