Local authentication
Local accounts are the break-glass and first-run path; SSO (OIDC for Okta and
Entra, LDAP) comes in phase 1b and will sit beside them. Everything here
lives in the data directory as JSON until the SQLite store arrives:
users.json, sessions.json, tokens.json (all mode 0600) and the
append-only audit/audit.jsonl.
First run#
logline serve with no accounts creates admin with a generated password
and prints it once to stderr. The password is not stored; the account is
flagged must change password, so the only thing it can do until then is
pick a new one. The same flag is set when an admin resets someone's password
or creates an account without supplying one.
Passwords#
argon2id with the second RFC 9106 parameter set (64 MiB, 3 passes, 4 lanes), PHC-encoded so the parameters can change later without invalidating stored hashes. The policy is length only: at least 12 characters, not the username. A login for an unknown user still runs the hash so timing does not reveal whether the account exists.
Lockout#
Five failed logins lock the username and the client address for one minute, doubling with each further failure up to an hour. Both keys are tracked so one attacker cannot lock out everyone and one victim cannot be attacked from many addresses. Lockouts are in memory; a restart clears them. Reverse-proxy headers are not trusted yet, so behind a proxy every client shares the proxy's address; put the proxy's rate limiting in front until the trusted-proxy setting lands.
Sessions#
The session cookie is a random 256-bit value; only its SHA-256 lives on
disk. HttpOnly, SameSite=Strict, Secure when serving TLS (or with
--secure-cookies behind a TLS proxy). Twelve hours absolute, two hours
idle. Sessions persist across restarts. A password change, a disable or a
delete ends every session of that account.
CSRF: on top of SameSite=Strict, every state-changing request from a
cookie session must carry X-Logline-Client: <anything>; a cross-site page
cannot add a custom header without a CORS preflight, which the server never
grants. Bearer-token requests do not need it.
Roles#
| Role | Can |
|---|---|
admin |
everything: users, inputs, tokens of others |
user |
search, see inputs and sourcetypes, manage own tokens, create dashboards and change own |
readonly |
search and view dashboards only |
Index-level access control (per-index read rights enforced in the planner) is phase 1b. The last active admin cannot be demoted, disabled or deleted, and nobody can demote, disable or delete themselves from the UI.
API tokens#
Created on the Account page or with the API, shown once, stored as a
SHA-256 hash, prefixed ll_ so leaks are easy to grep for. Scope search
can only call the search endpoints; scope full acts with the owner's
role. Optional expiry; last use is recorded. Tokens cannot create tokens or
change passwords.
curl -H "Authorization: Bearer ll_..." -H "Content-Type: application/json" \
-d '{"query":"index=web | stats count by status","earliest":"-24h"}' \
http://127.0.0.1:8000/api/searchAudit log#
audit/audit.jsonl under the data directory, one JSON object per line:
time, event, actor, how they were authenticated (session, token,
cli), client address, target, success, a short error, and the
request_id of the HTTP request that caused it (the same id is on the
engine's request log line and in the X-Request-Id response header).
Events: login, logout, password.change, user.create,
user.update, user.password.reset, user.delete, token.create,
token.revoke, search (with the query, range, limit, result count,
whether the cap was hit, elapsed time, the generated SQL and the
in-engine commands), service.restart, service.logs. Secrets never
appear. On an installed host the self-audit input tails the file into
index=_audit (SELF-LOGGING.md).
Endpoints#
| Method and path | Who | Purpose |
|---|---|---|
POST /api/auth/login |
anyone | {username, password}; sets the cookie |
POST /api/auth/logout |
session | ends the session |
GET /api/auth/me |
any | who am I, must-change flag |
POST /api/auth/password |
session | {current, new} |
GET/POST /api/users |
admin | list, create (password optional: generated and returned once) |
PATCH /api/users/{name} |
admin | {display_name, role, disabled} |
POST /api/users/{name}/password |
admin | reset; returns a temporary password when none given |
DELETE /api/users/{name} |
admin | delete with sessions and tokens |
GET/POST /api/tokens |
user | own tokens (?all=1 for admins); create {name, scope, ttl_hours} |
DELETE /api/tokens/{id} |
user | revoke own (admins: any) |
GET /api/dashboards, GET /api/dashboards/{id} |
any | list, read (DASHBOARDS.md) |
POST /api/dashboards |
user | create; the caller owns it |
PUT/DELETE /api/dashboards/{id} |
user | change or delete own (admins: any); PUT with the updated_at loaded answers 409 when it changed since |
GET /api/searches |
any, search tokens too | searches running now: own (admins: everyone's), with the caps |
DELETE /api/searches/{id} |
any, search tokens too | cancel a running search by its request id: own (admins: any); someone else's reads as 404 |
GET /api/v1, GET /api/v1/openapi.json |
any, search tokens too | the versioned API's index and its OpenAPI 3.1 description (JOBS.md) |
POST /api/v1/jobs, GET /api/v1/jobs |
any, search tokens too | submit a search job; list own (admins: everyone's) |
GET /api/v1/jobs/{id}[/results], DELETE /api/v1/jobs/{id} |
any, search tokens too | a job and its results; cancel and forget it. Own (admins: any); someone else's reads as 404 |
GET /healthz |
anyone | ok or degraded, nothing else |
Error bodies carry kind: auth (401, sign in), csrf, forbidden,
must_change_password, login (bad credentials; 429 with Retry-After
when locked), invalid (400, a dashboard the store refuses, with what is
wrong), conflict (409, a dashboard that changed since it was loaded), busy (429 with Retry-After: too many searches running,
or too many jobs held; see SEARCH-LIMITS.md and JOBS.md), not_found (404, a job). A search counts against the
account that runs it, so an API token's searches count against its owner.
Terminal#
logline users list
logline users add jdoe --role user --display "Jane Doe" # prints a temporary password
echo 'a-long-passphrase' | logline users add ops --role admin --password-stdin
logline users passwd jdoe # reset, temporary password
logline users role jdoe readonly
logline users disable jdoe
logline users tokens # every token, hashes never shownThe CLI writes to the same files as the server; it is the way back in when the only admin is locked out.