Sign in to the dashboard
Three authentication modes, one of which is only valid on loopback. Pick one, mint a credential, and know what reaches disk.
On this page
On your laptop the dashboard needs no credential, because it is bound to loopback and anyone who can reach it is already you. The moment it binds anything else, that stops being true, and the binary will not let you pretend otherwise.
This page covers the three modes, which to pick, and how to create a credential in each. Who can do what once they are in is roles and permissions.
Pick a mode
dashboard.auth.mode takes one of three values.
| Mode | Credential | Pick it when |
|---|---|---|
off |
none | the admin listener binds loopback and you are the only user. The default. |
token |
a bearer token | one operator, or a script, or a Grafana panel embedding a view |
users |
a session cookie backed by a local users file | more than one person, or you want per-person roles and an audit trail with names |
Whichever you pick, the mode governs the whole admin plane: the UI, the management API, and the exports. There is no separate setting for the API.
Token mode
One shared secret, presented as a bearer token. Set the mode:
dashboard:
enabled: true
auth:
mode: token
Then mint a token. It is printed once and only its SHA-256 hash reaches disk, so there is no command that will show it to you again:
$ tokentriage token new --role viewer --label "grafana panel"
token id: df48d71da95e75ea role: viewer
This token is shown ONCE and cannot be recovered:
ttk_Z-WSSTQBOaiu-lRKMUIFbAwmBAlC2INel0hl99y0gDs
That capture is from a throwaway state directory that no longer exists. Yours will differ, and
the value after ttk_ is the part you keep.
Present it as an Authorization header:
$ curl -s -H "Authorization: Bearer ttk_…" \
http://127.0.0.1:9090/tt/api/v1/capabilities
token list shows what exists without showing any secret, and token revoke takes effect
immediately:
$ tokentriage token list
ID ROLE REVOKED LABEL
df48d71da95e75ea viewer false grafana panel
Give a token the lowest role that does the job, and a label that says what it is for. A
--ttl makes it expire on its own, which is the better default for anything handed to a
person rather than baked into a deployment.
Users mode
Local users with per-person roles. Passwords are hashed with argon2id and sessions are minted server-side, so the cookie carries no credential material.
dashboard:
enabled: true
auth:
mode: users
session_ttl: 12h # default: 12h
Add a user. The password is read from stdin and never echoed, logged or written:
$ tokentriage user add alice --role operator
Password:
added user "alice" with role operator
$ tokentriage user list
NAME ROLE CREATED
alice operator 2026-08-10
The tree also has passwd, set-role and remove. All of them read and write users.yaml
under the dashboard state directory, which defaults to ~/.tokentriage/dash and is set by
dashboard.state_dir. Once the dashboard is running you can do the same work from the Users
panel in Settings, which requires the admin role.
Repeated failed logins are throttled per source address before the password check runs, so a spray costs the attacker time without costing your instance the work of hashing. A sustained run of failures is recorded in the audit trail.
What a denial looks like
Two different failures, and telling them apart saves time.
No credential, on a route that needs one gets a 401. Send one.
A credential whose role is below the route’s floor gets a 403 naming the role that was
required. It is never a 404: hiding the existence of a route you cannot use would make the
boundary impossible to reason about.
{"type":"/tt/api/problems/forbidden","title":"Forbidden","status":403,"detail":"route not permitted","code":"forbidden","request_id":"req_06754dd4d478f526"}
Every denial carries a request_id that also appears on the server’s log line for the same
request, which is what lets you correlate a user’s screenshot with your own logs.
Where the credentials live, and what to back up
Everything is under dashboard.state_dir, default ~/.tokentriage/dash:
| File | Holds |
|---|---|
users.yaml |
user names, roles, argon2id password hashes |
tokens.json |
token ids, roles, labels, expiry, SHA-256 hashes |
| the audit log | every attempted write, accepted or rejected, with its actor |
| the event ring | the durable mirror of successful mutations |
None of it is derived, so none of it rebuilds itself. Include the state directory in whatever backs up your ledger; backup and restore covers the rest of what a deployment holds.
What can go wrong
Validation rejects the config with a message about the admin bind. You have mode: off
and a non-loopback server.admin. Choose token or users.
A minted token does not work. Check that the mode is token; a token presented to an
instance in users mode is not a credential it accepts. Check the state directory too, since
token new writes to whichever directory it was given and the server reads the one in its
config.
Nobody can sign in after a restore. The state directory was not restored with the ledger. Mint a new token from the host to get back in, then re-add users.
You need to reach the API from a script and do not want a long-lived secret. Mint a token
with --ttl and re-mint on a schedule, or use a share token if the script only needs one
frozen query. Share a view covers the second option.