Account#
The Account page is the one page in the product every role can use in full. It shows who the agent thinks you are, lets you change your own password, enrols a second factor, lists the sessions your account holds and lets you end any of them. None of it is router configuration and none of it is staged: each action takes effect at once and is written to the audit log.
Identity#
The left panel is GET /api/auth/me, which is the first call the app makes and the answer
that decides whether you see the UI at all
(agent/authhttp.go handleMe).
| Row | What it tells you |
|---|---|
| Signed in as | The principal's name. For an API token this is token:<label>; for the break-glass token, admin-token. |
| Role | viewer, operator or admin. |
| Authenticated by | single sign-on when the session was opened through the identity provider, otherwise browser session. |
| Single sign-on | The provider's subject for this account, or not linked. |
| May change config | Whether the role is at least operator. |
| May manage users | Whether the role is admin. |
| Session expires | When this session ends. The default lifetime is 12 hours, set in agent settings. |
Two banners can appear above it:
- This account is still on the password it was created with. The
must_change_passwordflag, set when an admin creates or resets an account. Change it here before doing anything else — a shared initial password is not a credential. - This account has no password of its own. It exists as the Wheelhouse side of an identity provider subject. Set a password before unlinking, or there is no way back in.
Changing your password#
Three fields: the current password, the new one, and a confirmation. The button stays disabled until the two new ones match.
POST /api/auth/password checks the current password server-side and answers 403 — not
401 — when it is wrong, because the SPA treats any 401 as a dead session and would have
signed you out over a typo.
What a successful change does (agent/authhttp.go
handleChangePassword):
- rehashes with a fresh 16-byte salt (Argon2id, 64 MiB, 3 passes, 2 threads — about 60 ms
on an N100-class CPU,
agent/auth.go); - clears the must change password flag;
- deletes the first-boot
initial-passwordfile when you are the bootstrapped admin; - signs out every other session for your account and keeps the one you are using.
Choosing a password#
Any non-empty password of at most 1024 characters is accepted. There is no minimum length
and no complexity rule, deliberately: the field's own hint is any password you choose; a
passphrase beats a short scramble. A rule that pushes people towards Summer2026! buys
nothing.
What protects the login instead is rate limiting — 8 failures per source address in 5
minutes, 20 per account name in 30 minutes, and a site-wide ceiling of 200 failures in 5
minutes past which every attempt is slowed by 250 ms — and, when you turn it on, the second
factor below (agent/security.go).
Two-factor authentication#
TOTP as RFC 6238 specifies it: a 160-bit secret, SHA-1, six digits, a 30-second step, and a
window of one step either side of now so a phone whose clock has drifted slightly still
works (agent/auth.go totpCode, verifyTOTP).
The panel has one button: Set up when two-factor is off, Disable when it is on. The
enrolment flow it opens is the same component the mandatory gate uses, so the voluntary and
the compulsory paths are the same steps in the same words
(ui/src/components/TotpEnrolment.tsx).
1 — scan this. Opening the flow calls POST /api/auth/totp/begin, which mints a fresh
secret and returns it with its otpauth:// URI. The QR code is drawn on the page by the
router itself, so nothing about the account leaves the box and the screen works on a router
with no route to the internet —
ui/src/components/QrCode.tsx.
The panel says the entry will appear in the app as Wheelhouse: <your account>; the URI the
agent mints names the issuer Wheelhouse and the account
<your account>@<the router's host name>. Cannot scan? Type it in instead reveals the
secret in four-character groups and the whole enrolment URI, with the reminder that both are
passwords: anyone who copies them can generate your codes.
2 — prove it works. Type a current six-digit code and press Turn on two-factor,
which calls POST /api/auth/totp/confirm. A refused code says so and leaves you on the
form — check the clock on the phone first, which is the usual cause. Start over with a new
secret begins again with a fresh one.
The secret handed out in step 1 is stored as pending and gates nothing. Only a confirmed
code promotes it to the live secret and sets the enabled flag. That is what makes a
mis-scanned code harmless — and it is why merely starting a re-enrolment no longer disables
the second factor you already had
(agent/authhttp.go handleTOTPBegin, handleTOTPConfirm).
Disable calls POST /api/auth/totp/disable, which clears the secret, the pending secret
and the flag. It is refused with 403 when require_totp is on in agent settings.
The enrolment component has a third step — keep these — that shows recovery codes once and will not finish until you tick that you have saved them. It is written and it is not reachable, because the agent issues no codes for it to show. It stays there so the day the agent does issue them, the screen already handles them rather than printing numbers that would not work.
When two-factor is mandatory#
With require_totp on, an account that has no second factor still signs in — refusing
outright would lock the router, because enrolment itself needs a session — but what it gets
is a full-screen enrolment gate instead of the console: no navigation, no dashboard, and no
way past it but finishing or signing out. That is deliberate. Without the gate, the account
landed on a dashboard whose every panel answered 403
(ui/src/components/TotpGate.tsx).
The session such an account holds can reach exactly five routes:
/api/auth/totp/begin, /api/auth/totp/confirm, /api/auth/me, /api/auth/password and
/api/auth/logout. Everything else answers 403 with totp_enrolment_required and
enrol_at. The password route is in that list because the two gates stack: a freshly
created account arrives both must change password and enrolling, and with the password
route behind the enrolment gate it could do neither.
The gate is evaluated per request from the live policy and the live account, so switching
the policy on catches sessions that were already open, and switching it off releases them. A
single sign-on session is exempt — that factor belongs to the identity provider — and a
token or the break-glass credential is never gated
(agent/authhttp.go enrolmentRequired, enrolmentPaths).
An admin can turn the requirement off again under Administration → Agent settings. The router's own console login is unaffected by it either way.
Single sign-on#
This panel appears only when the agent has a provider configured. It shows the provider's display label, whether discovery currently answers, and this account's subject — with the error detail printed when the provider is not answering.
Unlink calls POST /api/auth/oidc/unlink and is disabled when the account has no
password, with the reason as its tooltip: unlinking the only credential leaves nobody able
to sign in. Full detail is on single sign-on.
The panel's own summary of the division of labour is worth keeping: the identity provider decides who you are, Wheelhouse decides what you may do, and revoking access at the provider takes effect at the next sign-in — not on the session already open.
Active sessions#
The table is GET /api/auth/sessions. A viewer or an operator sees their own sessions; an
admin sees everyone's (agent/authhttp.go handleSessions).
| Column | Notes |
|---|---|
| User | The account. Your current session carries a this browser badge. |
| Role | The role the session was issued with. |
| From | The address the session was opened from. Behind a proxy this is the last address in X-Forwarded-For, and only when the agent was started with --trust-proxy. |
| Client | The user agent string, truncated. |
| Signed in | Relative time. |
| Expires | Absolute time. |
| Revoke, or Sign out on your own session. |
The identifier shown is the first 12 characters of the session id, and revocation matches on that prefix — you can revoke your own sessions, and an admin can revoke anyone's. Revoking the current session signs you out.
Sessions end four other ways: they expire; you log out; you change your password (every other session goes); or an admin changes your role, your password or your provider subject (every session goes, including the one you are using).
See also#
- Roles — what the May change config and May manage users rows mean in practice.
- Users — the admin side: creating accounts, resets, and linking a subject.
- API tokens — for anything scripted, instead of your own session.
- Single sign-on — how an identity becomes an account.
- Accounts and sessions — the concept.
- Authenticating — cookies, CSRF and bearer tokens as a reference.
- First sign-in — what this page looks like on a new router.
- Locked out of the UI — when the second factor is gone.
Checked against#
ui/src/pages/Account.tsx,
ui/src/components/TotpEnrolment.tsx,
ui/src/components/TotpGate.tsx,
ui/src/lib/api.ts,
agent/authhttp.go,
agent/auth.go,
agent/security.go,
agent/admin.go,
agent/store.go,
agent/main.go.