Authentication & access

Truke KF resolves every request — a browser session, a single sign-on login, a request behind a corporate proxy, or an API call — to one user record in the KF database. That record carries the user's access labels (ACL). Authentication is how you prove who you are; the ACL is what you are allowed to see. The two are kept separate: no matter how you log in, access always comes from your KF user record.

Concepts

Identity. Each user has a stable uid (a short, space-free identifier — never reused, even after a user is removed), a display name, an email (always present, used for notifications), and an ACL label string. See access control for what the labels mean.

Login channels. There are four ways to authenticate. They can be enabled independently and used side by side:

ChannelWho it is forHow it works
Localsmall teams, the defaultusername + password stored in the KF database
OIDCone modern identity provider (Google, Entra, Okta, Keycloak)single sign-on; KF talks to the provider directly
Headerenterprises with SAML / LDAP / MFAa trusted reverse proxy authenticates and passes the identity to KF
TokenAPI and MCP clientsa personal access token sent as `Authorization: Bearer …`

Authorization is separate. None of these channels grant access on their own. A new user provisioned through SSO starts with whatever the default ACL policy says (by default, no labels — they see only unrestricted items) until an administrator grants labels. Identity providers never decide access directly.

Configuration overview

All authentication is configured in one `[auth]` section of kf.ini. Unlike the other sections, `[auth]` is written as an indented tree (the first line after the header has no `=`):

[auth]
session
  cookie_name kf_session
  ttl         24h
  secret_env  KF_SESSION_SECRET
local
  enabled     true
oidc
  enabled     false
header
  enabled     false
token
  enabled     true
groups
  enabled     false

Secrets are never written in kf.ini. Every secret is referenced by the name of an environment variable, through a key ending in _env. KF reads the value from the environment at start-up. For example `secret_env KF_SESSION_SECRET` means KF will use whatever KF_SESSION_SECRET is set to:

export KF_SESSION_SECRET="$(openssl rand -hex 32)"

If you enable a channel but leave a required field empty, KF refuses to start and prints what is missing — there is no silent half-configured mode.

Local accounts

This is the default and needs no setup. Credentials live in the KF database. Each user has a username, a password, and an ACL label string. Add, change, or remove users through the administration interface at `/admin/users`.

Local login is on unless you turn it off:

local
  enabled true

Users created by SSO have no password and cannot log in with the password form; they must use their single sign-on channel.

Single sign-on (OIDC)

OIDC delegates authentication to one external identity provider (IdP) — Google, Microsoft Entra, Okta, Keycloak, and similar. The user logs in at the provider; KF never sees the password. The login page offers both the password form and a single sign-on button, so local and SSO users coexist.

oidc
  enabled        true
  issuer         "https://accounts.google.com"
  client_id      kf-app
  client_secret_env KF_OIDC_SECRET
  redirect_url   "https://kf.example.com/oidc/callback"
  jit            true
  default_acl    ""
  uid_claim      preferred_username
  subject_claim  sub
KeyRequiredDescription
issueryesProvider base URL — KF discovers the endpoints at `…/.well-known/openid-configuration`
client_idyesClient ID issued by the provider
client_secret_envyesName of the env var holding the client secret
redirect_urlyesMust exactly match the callback registered at the provider: https://kf.example.com/oidc/callback
jitnoAuto-create users on first login (see below); default false
default_aclnoLabels given to auto-created users; default empty (no labels)
uid_claimnoToken claim used to derive the uid; default preferred_username
subject_claimnoStable claim used to match returning users; default sub

Set the secret in the environment before starting KF:

export KF_OIDC_SECRET="GOCSPX-…"

Just-in-time provisioning

With `jit true`, a user who logs in for the first time is created automatically. KF derives a uid from uid_claim (or the email local-part) and matches returning users on the provider's stable subject_claim — never on email alone, so a changed email address does not create a duplicate or hijack another account. The new user receives the default_acl labels (none, by default). Their name and email are kept in sync from the provider on every login; their ACL is not touched by SSO — it stays under KF's control.

With `jit false`, only users that already exist in the KF database may log in; unknown users are rejected. To pre-provision, add the user in `/admin/users` and leave the password empty.

Provider examples

# Google
issuer  "https://accounts.google.com"

# Microsoft Entra (Azure AD)
issuer  "https://login.microsoftonline.com//v2.0"

# Keycloak
issuer  "https://keycloak.example.com/realms/"

Register https://kf.example.com/oidc/callback as the redirect URI at the provider, and copy the client ID and secret into kf.ini / the environment.

Enterprise SSO via a reverse proxy (header channel)

KF deliberately carries no SAML, LDAP, Kerberos, or MFA code. When you need any of those, put a reverse proxy (nginx, Caddy, or Apache) with an authentication module in front of KF. The proxy handles the enterprise protocol, then forwards the verified identity to KF as HTTP headers.

header
  enabled        true
  user_header    X-Authenticated-User
  email_header   X-Authenticated-Email
  name_header    X-Authenticated-Name
  groups_header  X-Authenticated-Groups
  secret_header  X-Auth-Secret
  shared_secret_env KF_HEADER_SECRET
  trusted_proxy  127.0.0.1
  jit            true
  default_acl    ""

How KF stays safe when it trusts headers:

  • Shared secret. The proxy adds a secret header (X-Auth-Secret) carrying a value only it and KF know (shared_secret_env). A request whose secret is missing or wrong is rejected with 403, no matter what identity headers it carries. This is what stops a user from forging X-Authenticated-User.
  • Network binding. KF only honours the headers when the request comes from the configured trusted_proxy address.
  • Bind KF to loopback. KF must not be reachable except through the proxy (127.0.0.1:8080, or a firewalled private interface). If KF is directly reachable, the header channel is forgeable.

Generate the secret and share it on both sides:

export KF_HEADER_SECRET="$(openssl rand -hex 32)"

A minimal nginx terminator (paired with an OIDC/SAML auth module such as oauth2-proxy) injects the verified identity and the secret on every request, and strips any client-supplied copy:

location / {
    auth_request /oauth2/auth;
    auth_request_set $kf_user  $upstream_http_x_auth_request_preferred_username;
    auth_request_set $kf_email $upstream_http_x_auth_request_email;

    proxy_set_header X-Authenticated-User  $kf_user;
    proxy_set_header X-Authenticated-Email $kf_email;
    proxy_set_header X-Auth-Secret         "REPLACE_WITH_SECRET";

    proxy_pass http://127.0.0.1:8080;
}

The SAML/LDAP/MFA flow lives entirely in the proxy; KF only ever sees the headers. Provisioning works the same as OIDC: with `jit true`, users are created on first arrival with the default_acl policy.

To confirm the boundary, a forged request with no secret must never log you in:

curl -is https://kf.example.com/ -H 'X-Authenticated-User: attacker' | head -n1

API and MCP access tokens

For programmatic access — scripts, integrations, and MCP clients — KF issues personal access tokens. A token is bound to your user and carries exactly your access; it grants nothing extra.

token
  enabled true

Create a token (you must be logged in). The plaintext is returned once — store it now, it cannot be retrieved later:

curl -X POST https://kf.example.com/auth/tokens \
  -d name=ci-pipeline -d ttl=720h
# {"id":"a1b2c3d4","token":"f0e1d2…"}

Use a token by sending it as a bearer credential:

curl https://kf.example.com/mcp -H "Authorization: Bearer f0e1d2…"

Revoke a token using the id returned at creation:

curl -X POST https://kf.example.com/auth/tokens/a1b2c3d4/revoke

Tokens are stored only as a hash — KF never keeps the plaintext — and an expired or revoked token is refused with 401. The MCP endpoint (`/mcp`) and the token endpoints require authentication; they are never served anonymously.

Mapping provider groups to KF labels (optional)

Normally an administrator assigns ACL labels. If you would rather drive the initial labels from your identity provider's groups, enable the group map. It translates IdP group names to KF labels when a user is first provisioned; groups with no entry are ignored. This is a mapping you define in KF — raw provider groups are never trusted directly.

groups
  enabled true
  map
    QA-Automotive automotive
    Engineering   eng

A user whose provider groups include QA-Automotive is created with the

managed in KF and is not overwritten on later logins.

## Sessions and secrets

After a successful local or SSO login, KF sets a signed session cookie
(`cookie_name`, default `kf_session`) so later requests do not repeat the login.
Sign the cookie with a stable secret from the environment so sessions survive a
restart:

session cookie_name kf_session ttl 24h secret_env KF_SESSION_SECRET


export KF_SESSION_SECRET="$(openssl rand -hex 32)"


Header and token requests do not use the session cookie — they carry their identity
on every request.

## Quick reference

| Path | Purpose |
|------|---------|
| `/oidc/login` | Start a single sign-on login |
| `/oidc/callback` | OIDC return URL (register this at your provider) |
| `/auth/tokens` | `POST` to create a personal access token |
| `/auth/tokens/{id}/revoke` | `POST` to revoke a token |
| `/mcp` | MCP endpoint (requires a token or session) |
| `/admin/users` | Manage local users and ACL labels |