Skip to main content

API authentication & RBAC

GET /healthz is public. Everything under /v1 requires an Authorization: Bearer <token> header. There are three token kinds, all resolved by the same middleware (src/auth/middleware.rs).

Token kinds

TokenOriginScope
Bootstrap admin token~/.config/denia/admin.token (generated by denia setup)Super-admin across all projects. HMAC-SHA256 verified in constant time; never stored in the DB.
Session tokenPOST /v1/auth/loginA user's session (TTL in SQLite). Hashed before storage.
API tokenPOST /v1/api-tokensLong-lived named token for CLI/automation (denia auth). Hashed before storage.

Bootstrap → account

The admin token is exchanged once for a real account:

POST /v1/bootstrap
{ "username": "admin", "password": "<strong-password>" }

Thereafter, users log in (POST /v1/auth/login) for a session token, and mint API tokens (POST /v1/api-tokens) for the CLI. Inspect the current principal with GET /v1/me.

Project-scoped roles

Every user has a membership role per project, ordered Viewer (0) < Operator (1) < Admin (2). Each route enforces a minimum role for the target project; the super-admin bypasses membership checks. See Projects & RBAC, ADR-008.

CapabilityMinimum role
Read services/deployments/jobs/logs/metrics (env redacted)Viewer
Create/update services & jobs, deploy, manage domains/registries, open console, trigger runsOperator
Manage project members and rolesAdmin
Create/delete projects, manage users, registry GCSuper-admin

When a caller lacks the role, the API returns 403 Forbidden, or a 404-style message where revealing existence would itself leak information.

WebSocket exception

The service console websocket (GET /v1/services/{id}/console/ws) cannot carry an Authorization header from a browser, so it authenticates with a single-use 30s ticket minted over normal bearer-authenticated HTTP. Bearer tokens never appear in a websocket URL.