Users, roles and superadmins
isb serve keeps its own users. Every HTTP caller is one of them (or an
agent holding one of their tokens), and what a caller may do follows from
two things: its role in an org, and whether it is a platform admin or a
superadmin. This page explains those levels; signing in is in
Sign-in, and the endpoints are in the
Identity API.
| Level | Reaches | Typical holder |
|---|---|---|
| org viewer | reads one org | a stakeholder, a dashboard |
| org member, admin, owner | administers one org | the org's people, its workspace, its agents |
| org agent identity | one org, at a role the org gave it (never owner) | a tailnet node or Cloudflare Access service token an org maps (Agent identities) |
| platform admin | every org, plus creating and deleting orgs | the platform's operators |
| superadmin | everything the daemon's unix socket can do, the host included | the host's own operator, break-glass automation |
Roles#
The org is the trust boundary: a member of an org (member, admin or owner) fully administers that org's apps, stacks, sandboxes and secrets, and nothing in any other org. Each membership has a role:
| Role | List and inspect (read-only tools, logs, events) | Administer the org's apps and secrets: deploys, exec and the terminal, secret values | Manage members, invitations, every token in the org; read its audit log | Make and change owners |
|---|---|---|---|---|
viewer |
yes | |||
member |
yes | yes | ||
admin |
yes | yes | yes | |
owner |
yes | yes | yes | yes |
A viewer runs only tools annotated read-only (readOnlyHint) that hand out
no secret material: it can list and inspect stacks, apps, deployments, logs,
secret names and the org, and is refused writes, deploys, sandbox_exec
(and so the web terminal and SSH), secret_get, secret_resolve and
app_webhook. The check is the one authorizer every surface goes through, so
new tools are covered by their annotation.
- An actor grants roles up to its own: an admin can add viewers, members and admins; only an owner (or platform admin) can make or change an owner.
- An org always keeps one owner: the last one cannot be demoted or removed.
- Anyone can leave an org.
- Roles are stored as text and mapped to permissions in one table in code
(
Role::permissions), so a new role or a finer permission needs no migration.
Some org tools need more than membership: volume snapshots, schedules and restores are for the org's admins and owners (Volumes), and so are creating, changing and deleting the workspace (Workspaces). The audit log is for owners and admins (The audit log).
Agent identities#
An org can let agents in with no isb token, by where they connect from. An
org's owners and admins map identities to a role in that org
(agent_identity_set, or Settings, Agent identities in the web UI):
| Front door | A mapping names | Matches |
|---|---|---|
| tailnet | a login (someone@example.com) or a node tag (tag:agents) |
a peer on a tailnet --listen address, as tailscaled's whois says: a tagged node by its tags only, never its owner's login; any other node by its user's login |
| Cloudflare Access | a service token's client id, or the email of someone who is not an isb user | a verified Cf-Access-Jwt-Assertion on a listener Access guards |
- The role is
viewer,memberoradmin, neverowner, and at most the role of whoever sets it. Names are exact (emails, logins and tags case-insensitively; client ids as given), with no wildcards or domains.agent_identity_listshows them to any member; viewers read, owners and admins change. Setting and removing are audited (auth.agent_identity_set,auth.agent_identity_remove). - A matched caller is a synthetic principal named after its source,
tailnet:<login>(a tagged node:tailnet:<node>) oraccess:<email or client id>, with no isb account (no sessions, passkeys or tokens;POST tokensis403). It holds a role in each org that maps it and nothing anywhere else: a tool call for another org is refused,/orgs/<org>/mcppins its own org, and the platform,server_*, org-management and host tools are never reachable. It is never a superadmin; an identity on the superadmin lists is judged as a superadmin first. - The mapping alone decides. A tailnet login that is also an isb user's email is still the mapped principal, not that user, and gets nothing of the user's memberships. An Access email that is an isb user's is the opposite: it acts as that user with their real memberships, and cannot be mapped (the mapping is refused, and ignored if the account came later); make the user a member instead. The web form offers that inline ("Add the user to the org as the chosen role") to owners and admins when the refusal names an isb user.
- Some callers get in through a front door with no mapping, and the org's MCP
page counts them when it says whether Access or the tailnet sign-in is on:
a member user's Access email, a platform admin's (who reaches every org),
an identity on
--superadmin-access, and a login on--superadmin-tailnet.agent_identity_listreports them underavailable.reachas counts for any member, with the names (emails, logins, client ids) only for the org's owners, admins and platform admins, plus whether the caller is one. It also reportsavailable.public_url, the address Access sits in front of, which the Access card shows as<public URL>/orgs/<org>/mcp. - A request that carries a bearer token or a session cookie is judged by it alone; the identity is asked only of one that carries neither. The caller gets a role in each org that maps it, the highest if several of its tags do.
- It is ambient, like a cookie, so the superadmin defences apply: writes
need
X-Isb-Csrf: 1,/mcpneedsContent-Type: application/json, anOriginmust name theHost, and theHostmust be one of the server's names (the tailnet listen addresses, the node's MagicDNS names, the public URL's host). Refusals are audited (agent.refused). The tailnet identity comes from the TCP peer only, never a forwarded header. - Workspace tokens are unchanged: they stay the org's own actor.
Platform admins#
A platform admin (a flag on the user) can do everything in every org, and
alone creates and deletes orgs and changes their limits and egress
exceptions (Orgs): those are what keep
orgs apart, so an org's own owners see them read-only. The first user
created is always a platform admin and owner of the default org.
Platform admins also:
- manage users from the web UI's Platform page or the
admin/usersendpoints: disable and enable them, and make or unmake platform admins. Nobody does either to themselves, and an enabled platform admin always remains; - are the only callers of
server_status,org_list,org_create,org_update,org_delete, theserver_*tools,audit_verify,registry_gc,notification_settings, adding and removing template catalogs, and re-encrypting every org's secrets, whatever their role in an org (an org owner's token is refused); - see every org's audit log and history, host-level rows included.
A platform admin is still a remote caller: its specs are held to the remote-spec policy, and it cannot reach instances isb does not manage. That is the line between a platform admin and a superadmin.
Superadmins#
A superadmin has what the daemon's unix socket has: every tool, no remote-spec policy, any instance, plus the host tools, which nobody else gets, platform admins included:
| Host tool | Does |
|---|---|
host_inventory |
every incus project and instance on the host, isb's or not: project, org, type, status, addresses, isb's stack and owner labels |
host_policy |
how the daemon serves: listen addresses, Access, the remote tool policy, what remote specs may ask for, and each superadmin source with its allow list and token count |
superadmin_token_list, superadmin_token_revoke |
superadmin tokens' metadata, and revoking one by id |
Four sources grant it, and nothing else:
| Source | Who | Audit name |
|---|---|---|
| the unix socket | the daemon's own user | local(uid N) |
| a superadmin token | Authorization: Bearer isb_sa_... |
token:<name> |
| a tailnet identity | a peer on a tailnet --listen address whose login or node tag is on --superadmin-tailnet |
tailnet:<login> (a tagged node: tailnet:<node>) |
| a Cloudflare Access identity | a verified Cf-Access-Jwt-Assertion whose email or service token client id is on --superadmin-access |
access:<name> |
isb serve logs at start-up which superadmin sources are on, and the web
UI's Host pages show them (The web UI).
Turning on the tailnet and Access sources is in
Reach isb serve remotely.
Superadmin tokens#
Superadmin tokens belong to nobody. They are minted only on the host, by the
daemon's user, which writes isb.db directly:
isb token create break-glass --superadmin --expires 30d # prints the token once
isb token ls # superadmin tokens as sa-ID, SUPERADMIN for a user
isb token revoke sa-3
--superadmin takes no --org, --user or --scope. No HTTP caller can
mint one, a superadmin included (POST tokens with "superadmin": true is
403), so a stolen HTTP credential never turns into a durable one. They are
revoked with isb token revoke sa-ID, the web UI's Host page, or
superadmin_token_revoke. Names are unique, 1 to 64 of [A-Za-z0-9._-].
Minting and revoking are in the audit log.
Tailnet and Access superadmins#
A tailnet or Access superadmin whose login or email is an enabled isb user acts as that user (its memberships, its account pages) plus superadmin. Anyone else, a superadmin token and a tagged node or service token included, is a synthetic principal named after its source, with no account of its own (no sessions, passkeys or tokens;
POST tokensis403).Both are ambient, like a cookie: a browser on a tailnet machine, or one holding the
CF_Authorizationcookie, sends them with any page's request. So, for them:- writes must carry
X-Isb-Csrf: 1; /mcpmust beContent-Type: application/json(a cross-site page sends that only after a CORS preflight, which isb never grants);- an
Origin, when sent, must name the request'sHost; - the
Hostmust be one of this server's names (the listen addresses, the public URL's host, the node's MagicDNS names), which blocks DNS rebinding.
The web terminal's
Origincheck applies as for a session. A request that fails these is refused and recorded (superadmin.refused). An MCP client that sends a foreignOriginis refused as a superadmin.- writes must carry
A tailnet identity comes only from the TCP peer address, asked of the local tailscaled (
whoisover its LocalAPI socket, else thetailscaleCLI on macOS), cached for a minute per address. Forwarded headers (X-Forwarded-For,Tailscale-User-Login) are never read. A tagged node is its tags, never its owner's login. When tailscaled does not answer, nobody is a tailnet superadmin, and the daemon says why once.An Access identity counts only from an assertion the daemon verified, on the loopback listeners Access guards; emails match case-insensitively and exactly, service tokens by client id, with no wildcards or domains.
--superadmin-accessis refused without Access configured and without--public-url(an Access superadmin'sHostmust be it).A request carrying a bearer token is judged by the token alone; a tailnet or Access superadmin is signed in ahead of any session cookie, and signing out does not change who it is.
Every call a superadmin makes over HTTP is recorded, reads included, with actor kind
superadminand its source as the actor; refusals too. A sandbox it creates is labelledisb.owner=<source>.
On an org-bound endpoint#
A superadmin's reach is the unbound /mcp and /api/v1/tools/<tool>. On an
org-bound endpoint (/orgs/<org>/mcp, /orgs/<org>/api/v1/tools/<tool>, the
terminal, SSH and workspace resources under /orgs/<org>/) a superadmin over
HTTP, and a platform admin, acts as an admin of that org only, so an org
connector in an MCP client behaves as an org connector whoever adds it:
tools/listthere is an org admin's list: the host, superadmin and platform tools are absent, and calling one is refused.- The remote-spec policy applies (no privileged containers, raw incus config,
host binds or instances isb does not manage), and
orgis pinned. - A platform admin or superadmin gets the admin role in that org. A user who is also a member keeps the higher of their own role and admin, so an owner stays an owner; everyone else keeps their own role.
- The audit row names who it really was (a superadmin's source, or the user)
and records
scope: org <org>in its details; reads are recorded too. - The unix socket is always full, on any path. Workspace tokens, org tokens and agent identities are unchanged.
How callers sign in#
| Credential | Used by | Notes |
|---|---|---|
Session cookie isb_session |
the web UI | from password, passkey or provider sign-in; cookie-authenticated writes carry X-Isb-Csrf: 1 |
API token, Authorization: Bearer isb_tok_... |
agents, scripts, the CLI against a remote daemon | from isb token create or the web UI |
Workspace token isb_ws_... |
the agents in an org's workspace | delivered inside the workspace, never shown (Workspaces) |
| Cloudflare Access | anyone, when Access is in front | an Access identity whose email belongs to an isb user acts as that user; one that does not gets "ask an org admin to invite you" |
| Tailnet or Access agent identity | agents an org maps | Agent identities |
| Superadmin sources | operators | above |
Anonymous callers are refused, unless the daemon runs with
--allow-unauthenticated (local testing only): a request with no credential
to /mcp, /orgs/<org>/mcp, /api/v1/tools or the events stream gets 401
with WWW-Authenticate: Bearer, so not even the tool list is shown. The unix
socket needs no token; the machine's users are its callers.
API tokens#
Authorization: Bearer isb_tok_.... A token belongs to a user and is
either:
- org-scoped: confined to one org, with the user's current role in it
(read on every request, so a demotion or removal applies at once; leaving
the org deletes the user's tokens for it). Any member can make one for
their own org. A platform admin's org token acts as owner of that org and
is not a platform admin. This is what an agent working in one org should
hold:
isb token create agent --org acme, then point it at/orgs/acme/mcp(Agents and MCP). - platform (no org): the user's whole reach. Platform admins only; it stops working if the user stops being one.
Tokens can expire (--expires 90d, "expires": "90d") or not. Each records
when it was last used. Users list and revoke their own; an org's owners and
admins list and revoke any token in it.
A token cannot mint tokens. New tokens come from a signed-in browser
session, an Access or tailnet identity, or isb token create on the host;
an API token, a workspace token and a superadmin token are all refused
(POST tokens and the token_create tool alike). A token that could mint
another would survive its own revocation through the copy, and bounding the
copy's scopes or expiry would not change that: revoking a leaked token must
end what it can do.
Scopes#
A token with no scopes has its user's whole role: an agent in an org
administers the org's apps, as its people do. Scopes are an opt-in
narrowing, checked in the same authorizer as roles, and never widen a role
(a viewer's admin token still only reads):
| Scope | Allows |
|---|---|
read |
read-only tools, never secret material (as a viewer) |
deploy |
read, plus stack_deploy, stack_redeploy, stack_rollback, stack_scale, app_deploy, app_rollback, build_run |
admin |
everything the role allows (the same as no scopes) |
tool:GLOB |
tools whose name matches, e.g. tool:app_*, tool:stack_status |
A token may hold several; a call passes if any allows it. A scoped token
without admin cannot change accounts, tokens, invitations, members or SSH
keys (every state-changing /api/v1/auth/* request, and every account tool
that changes something, is refused), and tokens
whose scopes leave out sandbox_exec (read, deploy) get no terminal or
SSH. Give scopes with "scopes": ["deploy", "tool:app_*"] on POST tokens,
isb token create --scope deploy --scope 'tool:app_*', or the web UI's
Access choice (full, deploy, read only, or only some tools).
A request carrying Authorization is judged by it alone: a bad token is a
401 even with a valid session cookie.
What each caller may reach#
overview,eventsandstack_listshow only the caller's orgs.org_getis for the org's members.audit_listshows an org's owners and admins their org's entries and platform admins everything;audit_verifyis for platform admins.- The
server_*tools are for platform admins. A call for an org placed on a server is judged on the control plane, then again by the server's agent (Placement). - The unix socket is the daemon's own user and reaches everything; so does a
superadmin.
host_inventory,host_policy,superadmin_token_listandsuperadmin_token_revokeare for superadmins only. --allow-toolsand--deny-tools(names or globs, deny wins) choose which tools remote callers see at all; the local socket always has every tool (Security model).- Every call that changes something, every refusal, every secret read, sign-in, webhook delivery and terminal session is recorded in the audit log, and every controller event and incus lifecycle event in the history.