Studio in v1 supports local authentication only — usernames +
argon2id-hashed passwords listed in studio.yaml. RBAC is optional;
absent it everyone has full access. The audit log captures every
mutating call.
| Backend | Status | Notes |
|---|---|---|
| Local | ✓ shipped | auth.backend: local (the only valid value today). Users + argon2id hashes in studio.yaml. Hot-reloaded. |
| LDAP / AD | deferred (see below) | Not implemented in v1. The auth.backend enum only accepts local — even with the field present. |
| OIDC / SAML | deferred (see below) | Same as LDAP. |
The
studio.yamlschema enforcesauth.backend = "local"; setting anything else trips a zod validation error at config-load time and the previous valid config keeps serving. We'd rather fail loudly than silently treat an unknown backend as a no-op.
Studio's surface is small — one operator console for one cluster. The point of v1 is the rule-management UX itself, not an enterprise identity matrix. Local auth covers:
- Internal-network deploys behind a corporate SSO at the network edge (an Identity-Aware Proxy / oauth2-proxy / Pomerium in front handles the actual SSO; Studio is one local user behind that).
- Air-gapped / on-prem evaluations.
- Per-team operator groups where a 2-line YAML edit beats a directory integration.
LDAP and OIDC will land as alternative auth.backend values:
auth.backend: ldap—auth.ldap.{url, bindDN, bindPassword, userSearchBase, groupSearchBase}. RBAC roles map from LDAP groups (configurable). No password hashes instudio.yaml.auth.backend: oidc—auth.oidc.{issuer, clientId, clientSecret, redirectUri, scopes}viaopenid-client. Roles map from a chosen claim (groups/roles/ custom).
Both are pure-additive — the local backend keeps working unchanged.
Until they ship: front Studio with an Identity-Aware Proxy that
terminates SSO and forwards a single shared local credential. That's
the documented production pattern in docker.md.
-
Generate an argon2id hash:
pnpm -F @vantage-studio/bff vsadmin:hash 'my-new-password' # or, in the running container: # docker exec -it studio /nodejs/bin/node /app/server.js --hash 'my-new-password' # (the --hash entrypoint is wired in v0.2)
stdinworks too:echo -n 'my-new-password' | pnpm -F @vantage-studio/bff vsadmin:hash
-
Add the entry to
studio.yaml:auth: local: users: - username: alice passwordHash: '$argon2id$v=19$m=65536,t=3,p=4$...' roles: [operator]
-
Save. Studio's config watcher picks up the change within ~1 s; no restart needed.
Delete the entry. Any active session for that user keeps working
until it expires (session.ttlMinutes). If you need to revoke
immediately, restart the BFF — sessions are in-memory, so a restart
clears every active session.
RBAC is opt-in. Without an rbac: block in studio.yaml, every
authenticated user has the wildcard * verb — full access. That's
the right default for trial deployments and single-team operator
groups.
Turn it on by adding the block:
rbac:
enabled: true
roles:
admin:
verbs: ['*']
operator:
verbs:
- rule:read
- rule:write
- rule:write:structural
- rule:delete
- rule:debug
- cluster:read
- inspect:read
viewer:
verbs:
- rule:read
- cluster:read
- inspect:readA user's effective verbs are the union of their assigned roles' verbs. Roles are assigned in the user list:
auth:
local:
users:
- username: alice
passwordHash: $argon2id$...
roles: [operator]
- username: bob
passwordHash: $argon2id$...
roles: [viewer]The current user's effective verbs are visible in the Permissions page (left nav, bottom).
| Verb | Routes it gates |
|---|---|
rule:read |
catalog browse, single-rule fetch, dump, OAL catalog browse |
rule:write |
addOrUpdate (filter-only), inactivate |
rule:write:structural |
addOrUpdate with allowStorageChange=true or force=true; revertToBundled delete |
rule:delete |
delete (default mode) |
rule:debug |
live debugger — start / poll / stop debug sessions across MAL / LAL / OAL |
cluster:read |
cluster matrix, dsl-debugging status pane |
inspect:read |
Inspect (SWIP-14) — /api/inspect/{catalog,metrics,entities,mqe-target,exec} |
admin |
(reserved — audit-read in a later release) |
* |
all of the above (wildcard) |
Highlights:
rule:write:structuralis the gate on every schema-change action —addOrUpdatewithallowStorageChange=true,force=truerecovery, andrevertToBundleddelete. Operators who only need filter-of-existing-metric edits can holdrule:writealone.- Default
deleteis not schema-changing in v1 — the row is removed and any backend resource is left as an inert artefact.rule:deletecovers it. The destructive path isrevertToBundled, which needs bothrule:deleteandrule:write:structural.
Every mutating call lands in the JSONL file at audit.file (default
/data/audit.jsonl in the Docker image). One line per event:
{
"level": "info",
"ts": 1730000000000,
"action": "addOrUpdate",
"verb": "rule:write",
"actor": "alice",
"outcome": "filter_only_applied",
"details": { "catalog": "otel-rules", "name": "vm", "allowStorageChange": false, "force": false },
"fromIp": "10.0.0.42",
"sessionId": "xy…"
}Failed attempts are recorded too:
{"level":"info","ts":...,"action":"addOrUpdate","verb":"rule:write","actor":"alice","outcome":"storage_change_requires_explicit_approval",...}
{"level":"info","ts":...,"action":"login","actor":null,"outcome":"bad_password","details":{"username":"alice"},...}Daily rotation is your job — point a logrotate config or a
sidecar log shipper at the file. Studio doesn't rotate; the line
volume is low (one event per operator action) so files stay small.
- Opaque 32-byte session id in a
HttpOnly+SameSite=Strict+Securecookie. - Server-side state is an in-memory
Mapper BFF process. Logout deletes the entry. - TTL is absolute (
session.ttlMinutes); there's no sliding window. - Sessions are lost on BFF restart — operators re-log in. For an internal tool that's the right trade-off; the alternative (server-side persistent sessions or signed JWTs with denylists) adds complexity that doesn't pay back.
- v1 is single-replica. Multi-replica HA needs a shared session store; revisit when there's a request.