Skip to content
Loglinepre-release
Menu
docs/AUTH.mdEdit on GitHub

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/search

Audit 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 shown

The CLI writes to the same files as the server; it is the way back in when the only admin is locked out.