Skip to content

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:

{
  "max_total_sandboxes": 60,
  "max_total_mem_mib": 96000
}

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_seconds for unattended jobs that shouldn't be able to camp the host.
  • Burst-friendly max_sandboxes but tight max_mem_mib so 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.