Roles#
There are three roles and they are enforced per route, server-side. A principal — a signed-in person, an API token, or the break-glass token — carries exactly one, and every route in the agent names the minimum role it needs. The web UI greys out what your role cannot do, but that is a courtesy: the check that matters happens in the agent, and a request that gets past a greyed-out button is refused and recorded.
viewer < operator < adminThe comparison is a rank, so an admin satisfies every check an operator satisfies, and an
operator satisfies every check a viewer satisfies
(agent/auth.go Role.AtLeast).
What each role may do#
| Viewer | Operator | Admin | |
|---|---|---|---|
| Read operational state — interfaces, routes, sessions, logs, the audit log | yes | yes | yes |
| Read the configuration tree | yes, redacted | yes, redacted | yes, in full |
| Read its own sessions | yes | yes | all sessions |
| Change its own password, enrol two-factor | yes | yes | yes |
| Stage, commit, commit-confirm, discard, roll back | no | yes | yes |
| Save the boot configuration, load a configuration | no | yes | yes |
| Reconcile against a desired-state file | no | yes | yes |
| Install, update, restart or remove an app | no | yes | yes |
| Ask for an inline IPS plan; run a traceroute | no | yes | yes |
| Write to a fleet member | no | yes, with a licence that includes fleet | yes, same |
| Create, change and delete accounts | no | no | yes |
| Issue and revoke API tokens | no | no | yes |
| Change agent settings, enter a licence | no | no | yes |
| Reboot, power off, manage boot images | no | no | yes |
| Download the unredacted configuration | no | no | yes |
How the route table says it#
The wrappers in agent/main.go routes are the whole authorisation
model, and it is readable in one screen:
| Wrapper | Means |
|---|---|
readOnly(h) | requireRole(RoleViewer, h) — any authenticated principal. |
writeable(h) | requireRole(RoleOperator, h). |
adminOnly(h) | requireRole(RoleAdmin, h). |
licensed(h) | writeable(requireLicense(h)) — the operator role and a usable licence. |
The counts, at the version of the tree this page was checked against: 107 route patterns —
64 readOnly, 4 writeable, 18 licensed, 15 adminOnly, and six that take no
authentication at all. GET /metrics is registered either way depending on
--metrics-public, and is counted once, in readOnly. Re-derive them at any time:
grep -o 'mux.HandleFunc("[A-Z]* [^"]*", [a-zA-Z]*' agent/main.go | sortThe licence gate is not a role#
Eighteen routes are wrapped in licensed, which is the operator role plus a usable licence.
An unlicensed agent answers 402 on those and keeps serving every read, the audit log and
the login screen. It is a commercial gate, not a security boundary
(agent/license.go requireLicense).
Two write-plane routes are deliberately not licence-gated: POST
/api/diagnostics/traceroute and POST /api/ids/ips/plan are operator-only but need no
licence, because neither changes configuration — one runs a command, the other renders a
plan.
The redaction boundary#
Configuration reads below the admin role are passed through redactSecrets, which blanks
the value of any leaf whose name is a credential — private-key, pre-shared-key,
password, secret, key and a dozen others — and replaces it with [redacted]. It wraps
GET /api/config, /api/config/commands, /api/wireguard, /api/vpn/ipsec,
/api/vpn/openvpn, /api/pki, /api/drift, /api/history/diff and the fleet config read
(agent/security.go, agent/main.go).
Two boundaries are drawn differently on purpose:
GET /api/stagedredacts below operator, not admin. The operator who staged a key typed it; showing them[redacted]in their own diff is not a preview. A viewer has no such claim on it.GET /api/config/rawis admin-only rather than redacted. A whole-configuration download with the secrets in it is the backup export, and it is not a read-only action.
What is unauthenticated#
Six route patterns answer without a session or a token, and they are the whole of it —
plus /metrics, when the agent was started with --metrics-public:
| Route | Returns |
|---|---|
GET /health | {"status":"ok"}. Deliberately not the version. |
GET /api/auth/status | Whether an account exists, whether TLS is on, whether the agent is licensed, whether two-factor is required, and single sign-on's display label and login path. |
GET /api/oidc/status | The same plus the issuer URL and redirect URI — more than the line above withholds. |
GET /api/oidc/login, GET /api/oidc/callback | The sign-on redirect and its return leg. |
POST /api/auth/login | The login itself, behind three rate limiters. |
GET /metrics | Only when the agent was started with --metrics-public; otherwise it is readOnly. |
What a refusal looks like#
An unauthenticated request gets 401 with a reason. A request from a principal whose role
is too low gets 403 with this action requires the <role> role, and an audit entry:
{"operation":"denied POST /api/stage","actor":"alice","role":"viewer","success":false,"error":"requires operator"}That entry is why the greyed-out button is a courtesy. An attempt to use a route your role
does not reach is recorded with your name on it
(agent/authhttp.go requireRole).
An operator on an unlicensed agent gets 402 with the licence state in the body.
A browser session belonging to an account that has no second factor while
require_totp is on gets 403 with totp_enrolment_required and enrol_at on every route
except five: /api/auth/totp/begin, /api/auth/totp/confirm, /api/auth/me,
/api/auth/password and /api/auth/logout. The gate is evaluated per request against the
live policy and the live account, so turning the policy on bites sessions that were already
open and turning it off releases them. A single sign-on session is exempt — that second
factor belongs to the identity provider — and a token or the break-glass credential is never
gated, which is the way back when somebody has locked themselves out of the UI
(agent/authhttp.go enrolmentRequired). See
Account.
Where a role comes from#
| Principal | Role from |
|---|---|
| A browser session | the account's role, copied into the session when it was issued |
| An API token | the role chosen when the token was issued |
| The break-glass token | always admin |
| A single sign-on user | the account's role, which group mapping may set — see single sign-on |
A session carries the role it was issued with, which is why changing an account's role drops
its sessions: the agent will not let a role change be outlived by an open session
(agent/admin.go handleUpdateUser).
See also#
- Users — where roles are assigned.
- API tokens — the same three roles, for automation.
- Account — what your own session says about your role and capabilities.
- Read, write and admin planes — the concept.
- Roles and what they reach and what a read hides — the reference tables.
- The HTTP API — every route with its guard.
- System — audit — where a denied request is recorded.
Checked against#
agent/main.go,
agent/auth.go,
agent/authhttp.go,
agent/security.go,
agent/license.go,
agent/admin.go,
docs/security.md.