Accounts your security team will accept#
Named accounts, roles enforced on the server, and an entry saying who changed what from where — the requirements that decide whether a network team can put a GUI firewall in front of an auditor. What is built, and what is not, is below.
Accounts#
Argon2id, 64 MiB over three passes with a 16-byte random salt per account — about 60 ms on an N100-class CPU. Only the hash and the salt are stored.
Any non-empty password is accepted. There is no minimum length and no complexity rule. The product advises instead: the change-password field says a passphrase beats a short scramble, and the installer, given fewer than eight characters, says so once and offers Use it or Change it. Neither refuses.
What protects the login is rate limiting, and there are three limiters because each stops a different attack: eight failures in five minutes per source address; twenty in thirty minutes per account name, so guessing one password costs attempts rather than addresses; and site-wide, past 200 failures in five minutes every attempt is slowed by 250 ms. The site-wide one never refuses, because a real operator has to be able to sign in during an attack.
Changing a password signs out that account's other sessions.
Two-factor#
TOTP (RFC 6238), enrolled per account and verified with a live code before it is switched on, so a mis-scanned QR cannot lock anyone out.
A setting makes it mandatory for everyone, and turning that setting on is safe in both
directions. An account that has not enrolled meets an enrolment screen standing in front
of every route, ahead even of the forced password change, because an account that had to
do both would otherwise sit in a password form whose submit was refused. And the agent
refuses the setting outright — 409, with the reason — when no admin account can still
sign in to complete the enrolment.
The gate is in the agent, not in the interface
Four roles, enforced server-side on every route. The interface greys out what your role cannot do; the refusal happens whether or not the button was grey.
A denied request is recorded in the audit log as denied <method> <path> with the
actor's name, so an attempt that got nowhere is still an attempt somebody can read about.
Roles#
| Role | May do |
|---|---|
viewer | Read operational and configuration state. Configuration reads are redacted: private keys, pre-shared secrets and password hashes come back as the literal [redacted], square brackets included. |
approver | Everything a viewer can, plus commit, commit-confirm and roll back — somebody else's staged change. It cannot stage, discard or reconcile, and it sees the staged set redacted: approving a change is agreeing to it, not being handed the credentials inside it. |
operator | Everything an approver can, plus stage, un-stage, discard, configure, reconcile and install apps. |
admin | Everything an operator can, plus accounts, API tokens, agent settings, the licence, power and boot images, and the unredacted configuration download. |
Per-object permissions do not exist. Four roles is the whole model.
Sessions#
Opaque random identifiers in a cookie: HttpOnly, SameSite=Strict, and Secure
whenever the request arrived over TLS. The identifier means nothing on its own; the
session record lives on the router. Default lifetime is 12 hours.
A cookie alone can never change configuration — every mutating request must also echo
that session's CSRF token in X-Wheelhouse-CSRF. You can see and revoke your own live
sessions from the Account page.
API tokens#
For scripts, Terraform and CI. Created from Admin → API tokens, they carry a role, may be given an expiry, and only their SHA-256 is stored — the plaintext is shown exactly once.
Single sign-on#
The agent is an OIDC relying party: authorization code with PKCE, ID tokens verified
through a maintained OIDC library, identities linked by the provider's sub claim, and
optional group-to-role mapping.
Two things to know. It is configured by flags on the systemd unit rather than through the interface. And password, TOTP, token and break-glass logins all keep working when SSO is on, so a provider outage does not make the router unmanageable.
Identities are never linked by email address unless you explicitly turn that on, and even
then only when the provider asserts email_verified.
The audit log#
Every change that reaches the router records the actor, their role, their source address, the commands it sent and whether the router accepted them. Denied requests are recorded too, and so is discarding a working set somebody else staged. Fleet writes are included — the most privileged thing the API does is recorded on both the success and the failure path.
Staging is not audited, and that is deliberate: adding a line to the working set changes nothing on the router, and a log of every keystroke in a form would bury the commits. The commit entry carries the commands, so what reached the router is what is recorded.
It lives in its own file, audit.jsonl, one JSON object per line, appended and never
rewritten, rotating at 8 MiB with one previous generation kept — so an audited action
writes one line rather than rewriting the accounts, tokens and sessions beside it. The
served tail is retained per policy: 2000 entries by default, adjustable.
Break-glass#
A file can hold a token that authenticates as admin without an account. The shipped unit points at a path and nothing creates the file, so a default install has no break-glass token; the agent logs that it is absent and carries on. Create one deliberately if you want one, and treat it as what it is: a full admin credential sitting in a file.
Read on#
- Security — the posture in summary.
- The wiki: accounts, single sign-on with Authentik, the audit log.
Try it against your own directory
OIDC with group-to-role mapping, three server-enforced roles and an attributed audit entry work on an unlicensed router today.