Auth Modes¶
Klangk's KLANGKD_AUTH_MODES setting is the single knob that selects how users
authenticate. The same application binary supports four modes, and each one maps
to a real-world deployment profile — there is no architecture change per
customer, only configuration.
| Mode | Login method(s) | Deployment profile |
|---|---|---|
none |
none — auto-login | local-dev (single user, your own browser) |
oidc |
SSO buttons only | customer-locked |
password |
email/password only | small team |
both |
SSO buttons and email/password | team |
The default is none. A fresh klangk with nothing set boots in
no-login single-user mode, bound to loopback — it "just works" locally with
no password and is unreachable from the network. See
The default below for the upgrade implications.
Choosing a mode¶
none— the default. You run klangk on your own machine for development or testing and don't want to type a password. The server auto-logs you in as the seeded default user (see no-auth mode below). Must bind loopback.password— a small trusted group logs in with email/password.oidc— your organisation manages identity through an OIDC provider (Keycloak, Okta, Azure AD, …) and you want to disable local passwords. See OIDC.both— SSO for most users, plus email/password as a fallback.
The default¶
KLANGKD_AUTH_MODES defaults to none — a fresh klangk with nothing
configured boots in no-login single-user mode, bound to loopback
(127.0.0.1). It "just works" locally: open the browser, you're in, no
password. OIDC settings (KLANGKD_OIDC_*) do not change this default —
configuring a provider only takes effect once the mode is oidc or both
(set explicitly). Set KLANGKD_AUTH_MODES explicitly to enable password,
OIDC, or combined login.
Upgrading from an earlier version: if you previously relied on OIDC being configured implying
both(the old "OIDC turns auth on" rule), your server will now boot innonemode instead — no-login single-user, loopback-bound. That is safe by construction —nonerefuses to start on a non-loopback bind (see why this is safe) — but you should setKLANGKD_AUTH_MODES=oidc(orboth) explicitly before redeploying to preserve your intended auth posture. See Switching modes.
Seeding behavior across modes¶
On every boot, the lifespan seeds a default admin row only when the admin
group is empty (first boot, or after every admin has been deleted).
What gets seeded depends
on auth_modes and (for password modes) whether KLANGKD_DEFAULT_PASSWORD
is staged. The two tables below are the acceptance matrix for this behavior.
Table A — first boot (fresh DB, admins group empty)¶
auth_modes |
bind | default_password |
behavior |
|---|---|---|---|
none (unset) |
UDS (no KLANGKD_PORT) |
n/a | Seed admin row, password_hash=None. No password minted, nothing printed. /auth/local works (loopback-token). |
none (unset) |
TCP loopback (KLANGKD_PORT + KLANGKD_LISTEN=127.0.0.1) |
n/a | Same as UDS row. /auth/local works from loopback. |
none (unset) |
TCP non-loopback (KLANGKD_LISTEN=0.0.0.0) |
n/a | Fail-fast: none mode refuses a non-loopback bind. |
oidc |
any | n/a | Seed admin row, password_hash=None. The seeded row exists for /auth/local and as a recovery identity. |
password |
any | set | Seed admin row with default_password's value (hashed). No print. |
password |
any | unset | Fail-fast: auth_modes=password requires KLANGKD_DEFAULT_PASSWORD (set it in klangkd.yaml or the env). Refuse to boot. |
both |
any | set | Seed admin row with default_password's value (hashed). No print. |
both |
any | unset | Fail-fast: same as password. |
The admin identity (default_user) defaults to <unixuser>@example.com,
derived from the invoking Unix user. Explicit KLANGKD_DEFAULT_USER (env or
klangkd.yaml) always wins.
Table B — subsequent starts (admins group non-empty)¶
The gate short-circuits
seeding once any admin exists — so default_user / default_password /
auth_modes changes after first boot have no effect on the seeded row.
Subsequent starts don't re-seed, re-password, or re-email. Changing the admin
after first boot is done via the in-app UI or klangk admin users *.
auth_modes (now) |
default_password (now) |
first-boot admin row state | behavior on restart |
|---|---|---|---|
| any | any | admin row exists with a real hash (was password/both at first boot) |
Seed skipped. Existing admin's email/password unchanged. Login uses the existing credentials. |
| any | any | admin row exists with password_hash=None (was none/oidc at first boot) |
Seed skipped. Existing admin row untouched — password_hash stays None. If auth_modes is now password/both, boot fails fast with "requires at least one admin with a password" — the Table B lockout guard fires before the server serves traffic, so the operator can't get into an unrecoverable state. Recovery: flip back to none mode (the null hash is fine there), use /auth/local to get an admin token, run klangk admin users set-password to set a real hash, then flip back to password/both. Or re-empty the admins group + reseed with KLANGKD_DEFAULT_PASSWORD staged. |
| any | set | admins group emptied between boots | Reseed from current default_user/default_password (delete-resurrection). Gating per Table A applies to this re-seed. |
No-auth mode (none)¶
none is the foundation for a no-friction single-user dev/test loop —
without standing up the multi-user tier or logging in each session.
Not supported with the published Docker host image. The host image publishes its browser port (
-p 8997:8997), which is network-reachable, whilenoneis loopback-only by design — the freely-issued admin token is safe only when solely the operator's loopback can reach/auth/local. Two gates refuse it in Docker: the bind-safety gate won't letnoneboot on a non-loopback bind, and even withKLANGKD_ALLOW_INSECURE_NO_AUTH=1the proxy/auth/localACL still denies the port-forwarded request with403(the request appears at the container as the Docker bridge IP, not127.0.0.1). The image therefore runspassword(oroidc/both). For a no-login single-user experience, run klangk locally (devenv, or the bare binary on your own machine) instead of the published image.
In none mode the server freely issues a JWT for the seeded default user
(KLANGKD_DEFAULT_USER, defaulting to <unixuser>@example.com, derived
from the invoking Unix user) with no credentials
required from the caller:
- The frontend calls
POST /api/v1/auth/localon load and stores the token, skipping the login form entirely. - The CLI (
klangk) probes the server's auth mode on each command (viaGET /config) and auto-calls/auth/localwhen it'snone, so noklangk loginis ever needed — the first command after registering the server withklangk login <server>just works (and re-registration isn't: a saved token that 401s triggers the same auto-login fallback). - Workspace terminals (WebSocket) flow the token through the
handshake's
Sec-WebSocket-Protocolheader, like every other WS client (#3201).
The freely-issued token is indistinguishable from a password-login token to
the refresh and blocklist machinery — it reuses the standard create_token
claims (sub, email, jti, exp) and the seeded default user is a real
database row.
Why this is safe¶
Two complementary controls keep none mode local:
-
Loopback bind gate. The server refuses to start in
nonemode unlessKLANGKD_LISTENis a loopback address (any of the IPv4 loopback range127.0.0.0/8, IPv6::1, or thelocalhosthostname). The loopback bind is the identity boundary: only the operator's own browser can reach/auth/local. To expose a no-auth server on another interface (e.g. an isolated throwaway VM), setKLANGKD_ALLOW_INSECURE_NO_AUTH=1explicitly — you will get a warning, and anyone who can reach that address is effectively logged in as admin. -
Proxy per-location ACL.
POST /api/v1/auth/localis wrapped in alocationblock that doesallow 127.0.0.1; allow ::1; deny all;. Workspace containers reach the host via pasta NAT and appear as the host's non-loopback IP, so a container hitting/auth/localis denied with 403 at the proxy — while the host browser (127.0.0.1) succeeds. The proxy itself stays bound to0.0.0.0(hosted apps and remote browsers rely on it). -
Backend source-IP self-check. As a third layer (and to close the front-proxy bypass), the
local_loginhandler independently verifies the effective client is loopback: it trustsX-Real-IP/X-Forwarded-Foronly when the immediate peer is itself a trusted (loopback) proxy, so a non-loopback caller can't spoof them. This matters when a loopback proxy (caddy, traefik, a sidecar) sits in front of the klangk proxy — then every proxied request has$remote_addr=127.0.0.1and the proxy ACL alone would admit everyone; the backend re-check catches the real client via the forwarded header and refuses non-loopback values.
Do not place a non-loopback proxy in front of the klangk proxy in
nonemode. A loopback front-proxy makes$remote_addrloopback for all requests, so the proxy ACL admits everyone; control #3 above still refuses non-loopback real clients viaX-Real-IP, so you stay safe, but the cleaner topology is to let klangk's own proxy be the edge or to use a real auth mode (password/oidc/both) when exposing the server beyond loopback.
Why the token is kept¶
Even though the token is free, every authenticated request still carries it as
a Bearer header. CORS already stops a cross-origin evil.com from reading
the /auth/local response (origins default to the hosting origin/localhost,
never *), so the token can't be stolen that way. The custom Authorization
header is then belt-and-suspenders CSRF defense: any endpoint that mutates
on a simple request (no custom header → no CORS preflight) is closed off,
because a forged cross-origin request can't carry the header. JSON
content-types already force a preflight; the token covers the un-audited
simple endpoints — cheap to keep, risky to drop.
Other modes¶
For password, oidc, and both, see Authentication
and OIDC configuration. These modes behave exactly as
before; none is purely additive.
Switching modes¶
Mode switching is just changing KLANGKD_AUTH_MODES and restarting —
users and data carry over untouched; there is no migration or re-seed
step. Two directions matter; both work because
the CLI probes the server's mode live on every command (it is not cached),
so the new mode takes effect the moment the server restarts.
none -> password / oidc / both (adding real login)¶
This is the common upgrade path: you've been running solo in none mode and
now want real logins (for yourself and/or teammates). One thing to sort out
first: the password for the default user. In none mode the seeded
admin has no password (password_hash=None — nothing checks it). Before
flipping to password/both, set one while you're still holding the free
admin token:
# 1. Still in none mode — you're auto-logged-in as the admin default user.
# Give that user a real password via the admin endpoint:
klangk admin users set-password admin@example.com
# 2. (Optional) invite teammates while you're still admin-with-token:
klangk admin invitations send teammate@example.com
# 3. Flip the mode and restart the substrate:
# (set KLANGKD_AUTH_MODES=password in your substrate env, then restart)
# 4. Log in for real — you and your invitees now use the login form / CLI:
klangk login
klangk admin users set-password resolves the email to a user id and
PATCHes the password (admin-gated). Run it while still in none mode,
when you're holding the free admin token — after the flip, the old free token
still authorizes until it expires, but it's simplest to do the password set
first. Confirm you're the admin with klangk status (it reports
admin: yes).
password / oidc / both -> none (dropping back to solo)¶
Going the other way needs no preparation — flip KLANGKD_AUTH_MODES=none and
restart (remember the loopback bind gate). Existing
issued tokens remain valid until they expire; once they do, the CLI auto-logs
in as the default user, and the browser skips the login form. No one is
locked out, because none requires no credential at all.
What carries over across a switch¶
- User accounts and their data are unaffected — modes only change how you authenticate, not what's stored. The same users, workspaces, volumes, groups, and ACLs survive every switch.
- Tokens already in flight keep working until they expire (or are blocklisted on logout). A mode switch is not a global logout.
- The seeded default user (
KLANGKD_DEFAULT_USER) is always present; innoneit's who you become, in the other modes it's a normal account.
One OIDC caveat¶
If you switch to oidc/both, users (including the default user) link to an
OIDC identity on their first SSO login, keyed on the identity's sub and
email. If the default user's seeded email doesn't match a real SSO account,
that first SSO login creates a new user row and the default user's solo-mode
data stays under the old email. To avoid orphaning data, set
KLANGKD_DEFAULT_USER to your SSO email before the first boot (it's
read only on first seed — editing it later has no effect), or assign
the default user's workspaces to the SSO identity via the admin API after
linking.