How people get accounts and sign in: registration modes, instance admins, email, LDAP and Active Directory, sessions, and the CLI's browser login
People sign in with a local password, with an LDAP or Active Directory account, or both. Scripts, the CLI, the desktop app and AI agents use access tokens. This page covers how accounts are created, how email is delivered, how instance admins are chosen and how sessions work.
[auth].registration decides who can create an account:
Mode | Who can register | Notes |
|---|---|---|
| Anyone who can reach the server, with the Sign up tab on | Invited people can also register from their invitation link. |
| Only people with an invitation to an organisation, using the email address it was sent to | The Sign up tab is hidden and |
What happens on registration depends on whether email is configured:
With email: Kribble sends a verification link (valid 24 hours). The account is created when the link is opened. Until then the username and email are only reserved.
Without email: the account is created immediately, without verification.
From an invitation: the account is created immediately, because the invitation proves the email address, and the person joins the inviting organisation.
Every new account gets a personal vault (an organisation of its own) with a space called My Notes. See Organisations and access.
invite_only needs a way in for the first person: an LDAP login, or an account created while registration was still open. And invitations to people without an account only arrive by email. With invite_only and no email or LDAP, nobody new can join.
An instance admin can open Instance admin in the user menu (/admin), list all users and organisations, promote and demote admins, and see the LDAP sources and system status. Being an instance admin does not give access to anyone's notes. Organisations and access describes the admin panel.
There are three ways to become one:
[admin].emails. At every start of kribble-web or kribble-builder, each listed address that belongs to an account is promoted. An address without an account is logged as a warning and skipped, so after someone registers (or first signs in through LDAP), restart a service to promote them.
The admin panel. An instance admin promotes or demotes others under Instance admin → Users. Nobody can demote themselves, so there is always at least one admin.
The first account. While [admin].emails is empty and no admin exists, the next account created becomes an admin.
The list is promote-only. Removing an address doesn't demote anyone, and a person demoted in the panel whose address is still listed is promoted again at the next restart. The Users tab marks these people as set by config.toml. To remove an admin for good, take the address out of the list and demote them in the panel.
Kribble sends three kinds of email:
When | Link valid for | |
|---|---|---|
Verify your email | Registration, when email is configured | 24 hours |
Reset your password | Forgot password on the sign-in page | 1 hour |
Join an organisation | An organisation member invites an address | 7 days |
Links point to [server].frontend_url, so set it to the server's public address. The sender is Kribble <[email].from>.
Choose a transport in [email] (all settings are in Configuration):
[email]
provider = "resend"
from = "kribble@example.com" # on a domain verified in Resend
[email.resend]
api_key = "<resend-api-key>"
[email]
provider = "smtp"
from = "kribble@example.com"
[email.smtp]
host = "smtp.example.com"
port = "587" # 465 = TLS, 587 = STARTTLS, 25 = none
username = "kribble@example.com"
password = "<smtp-password>"
# encryption = "starttls" # inferred from the port when empty
Kribble authenticates with SMTP AUTH PLAIN and refuses to send the password over an unencrypted connection unless the server is localhost. A relay that needs no login takes an empty username.
At startup, kribble-web logs the transport it chose, for example [email] provider=smtp from=kribble@example.com. provider=none means mail is only written to the log, as [email] (no provider) would send to=… subject=….
Without email:
New accounts are not verified.
Password reset doesn't work. A person who forgets a local password needs help from you (see Troubleshooting).
Invitations are still created. People who already have an account see them as a notification in Kribble. People without an account never receive the link.
A failed send is logged as [email] failed to send to …. For registration and password reset, the request then fails with an error. For invitations, the invitation is still created.
Add one [[ldap]] block per directory and set enabled = true. LDAP and local passwords work side by side.
The user types a username or email and a password on the normal sign-in page.
Each enabled source is tried in the order it appears in config.toml. Kribble binds with bind_dn (or anonymously), searches user_search_base with user_filter, and binds as the entry it found with the typed password. The first source where this works wins.
The entry must have attr_email and attr_username. Kribble finds the local account by email, then by username. If there is none, it creates one, with a personal vault, and no local password.
If the source has group_search_base, Kribble looks up the user's groups and applies [[ldap.team_map]] (see below).
If no source accepts the login, or the directory is unreachable, Kribble tries the local password. A directory outage logs LDAP auth failed, falling through to local, and accounts with a local password can still sign in.
Details:
The filter must match exactly one entry. More than one counts as a directory error, and the login falls through to the local password.
An existing local account that signs in through LDAP is tagged with the source's name (shown in the admin panel). The tag is never overwritten.
The display name comes from display_name_format (when attr_first_name or attr_surname is set), else attr_display_name, else cn. It only fills an empty display name; Kribble never overwrites one.
Each login opens a new connection. timeout_seconds (default 5) limits the connection and the searches.
An enabled source without url or user_search_base stops startup. The log shows LDAP: enabled sources=… when the sources load.
[auth]
registration = "invite_only" # recommended with LDAP
[[ldap]]
name = "primary"
enabled = true
url = "ldaps://ldap.example.com:636"
bind_dn = "cn=kribble-readonly,ou=service,dc=example,dc=com"
bind_password = "<service-account-password>"
user_search_base = "ou=people,dc=example,dc=com"
user_filter = "(|(uid=%s)(mail=%s))"
attr_email = "mail"
attr_username = "uid"
attr_display_name = "displayName"
group_search_base = "ou=groups,dc=example,dc=com"
group_member_attribute = "memberUid" # posixGroup
user_group_attribute = "uid"
[[ldap.team_map]]
group_dn = "cn=developers,ou=groups,dc=example,dc=com"
org = "acme"
teams = ["engineering"]
[auth]
registration = "invite_only"
[[ldap]]
name = "corp-ad"
enabled = true
url = "ldaps://ad.corp.example.com:636"
bind_dn = "CN=kribble,OU=Service Accounts,DC=corp,DC=example,DC=com"
bind_password = "<service-account-password>"
user_search_base = "DC=corp,DC=example,DC=com"
user_filter = "(&(objectClass=user)(|(sAMAccountName=%s)(mail=%s)))"
attr_email = "mail"
attr_username = "sAMAccountName"
attr_first_name = "givenName"
attr_surname = "sn"
group_search_base = "OU=Groups,DC=corp,DC=example,DC=com"
group_member_attribute = "member" # holds member DNs
user_group_attribute = "DN"
[[ldap.team_map]]
group_dn = "CN=Kribble Editors,OU=Groups,DC=corp,DC=example,DC=com"
org = "acme"
teams = ["editors"]
The group search matches the member attribute directly. Nested group membership is not resolved.
With group_search_base set, each successful login searches for groups whose group_member_attribute equals the user's user_group_attribute value (or full DN, with "DN"), narrowed by group_membership_filter if set. Each found group DN is compared, case-insensitively, with the group_dn of the source's [[ldap.team_map]] entries. For every match, Kribble makes the user a member of the organisation org and of each listed team.
The organisation and teams must already exist. Missing ones are skipped silently. Create them in Kribble first; the next login fills them.
Team names are matched exactly.
Sync only adds. Leaving a group doesn't remove anyone from an organisation or team; remove them in Kribble.
New organisation members get no permissions (see Organisations and access).
A failed group lookup is logged (LDAP: group lookup failed) and the login still succeeds.
Local passwords outlive LDAP accounts. Disabling someone in the directory doesn't stop a local password they already had. Accounts created through LDAP can't get one by password reset: Forgot password sends them nothing. To lock someone out completely, also remove them from your organisations in Kribble.
Accounts are not deleted when they disappear from the directory. People keep their personal vault.
tls_skip_verify turns off certificate checks for ldaps://. Use it for testing only.
The Auth Sources tab of the admin panel shows the loaded sources and their team mappings. The bind password is never shown. Changes need a restart.
Credential | Lifetime | What it is |
|---|---|---|
Access token | 15 minutes | A JWT signed with |
Refresh token | 7 days | A JWT signed with |
| 24 hours | Lets signed-in users open published sites that are limited to space members. |
| 1 hour | Lets the browser load uploaded files at |
Access token | until revoked or expired | For the CLI, CI, the desktop app's sync and MCP. See Organisations and access. |
Changing or resetting a password signs that person out of every browser. Changing jwt_secret or refresh_secret signs everyone out.
Sign-in is rate limited per client address: 10 requests a minute to the login, registration and token-refresh endpoints. Each failed login also adds a delay for that username and address (1, 2, 4, 8, then 16 seconds), cleared by a successful login. The client address comes from X-Forwarded-For when the request comes through a trusted proxy (see trusted_proxies in Configuration), so the reverse proxy must set it (Installing).
kribble login signs the CLI in without copying a token by hand, much like the OAuth device flow:
The CLI starts a login request (POST /api/v1/cli/login) and opens https://<server>/cli-login?code=<code> in the browser. Over SSH without a display, it prints the address to open instead.
The person, signed in to the web app, sees the computer's name and the requested scopes and chooses All my organizations (every organisation they belong to, including ones they join later) or a single organisation. They can narrow read,write down to read, and approve or deny.
Kribble creates an access token and encrypts it to a one-time key the CLI sent with its request. The CLI collects it by polling every 2 seconds. A request expires after 10 minutes and can be collected once.
The token is listed in the organisation's settings under Access Tokens (for All my organizations, in the personal vault's settings), where it can be revoked. The desktop app uses the same flow for Settings → Servers → Connect. For CI, create a token in the web app and use kribble login --token or the KRIBBLE_URL and KRIBBLE_TOKEN environment variables. See The kribble CLI.