Skip to content

Identity & access federation

alpstack federates user identity, application access, and mail from a single upstream identity provider (authentik). openDesk keeps no separate user database that must be maintained by hand: a user is created and entitled once, in authentik, and everything downstream follows automatically.

The model

  • authentik is the single source of truth — who a user is (identity), what they may use (group-based entitlements), and their name.
  • openDesk / Nubus (UDM) is a downstream directory kept in sync from authentik. Federated users live in a dedicated ou=federated so the sync never touches openDesk-native accounts.
  • Three pieces bridge the two:
Piece Runs Does
provision helper on demand (CLI) creates the user + assigns persona groups + name in authentik, and creates the matching directory user
directory importer in-cluster one-way sync of user identity from authentik's LDAP outpost into ou=federated — adopts existing entries only
access reconciler in-cluster (hourly) derives openDesk app access from authentik group membership and writes it into UDM; gives every user a baseline mailbox

The importer adopts; it does not create

A Nubus/UDM directory importer can only adopt directory entries that already exist and match by username. It cannot create them: a users/user create is rejected unless a primary mail address is supplied, and that is unconditional — it is not a side effect of the groupware flags, so turning groupware off does not avoid it. The reconciler cannot supply the address either, because it only runs once the object exists.

So something has to create the directory object. Here the provision helper does it, setting the primary address to the same <username>@<mail-domain> the reconciler would otherwise derive, so the two agree by construction. If you wire up only the importer and expect new users to materialise from the upstream IdP, they will not — and the failure is a quiet 422 in the importer log, not anything visible in the IdP.

Consequence worth planning for: the importer also cannot rebuild the federated tree from the IdP, so do not treat it as a disaster-recovery path for the directory.

Why a reconciler (and not just the importer)

openDesk grants per-app access through per-user attributes (the managed-by-attribute-* groups are read-only mirrors of those attributes — editing them directly is overwritten). Those attributes are booleans in UDM, and a plain directory sync can only carry strings, which UDM rejects. The reconciler translates authentik group membership into the correctly-typed attributes:

  • membership of an app-<component> group → the matching openDesk access attribute;
  • mail is a baseline — every provisioned user gets a mailbox, so it is not gated on a group;
  • only users carrying a persona are reconciled — a directory entry without one is left untouched (so a migration can proceed user-by-user without disabling anyone).

Personas

Access is assigned by persona — a named bundle of groups — not attribute by attribute. The reference deployment defines two:

  • family — all openDesk components + the mesh.
  • family-ext — mail + mesh only.

Adopters define their own personas in the provision helper and the authentik groups blueprint.

A new user, end to end

Creating a user in authentik with a persona yields — with no manual openDesk step:

  • SSO login — a login is redirected straight to authentik; there is no intermediate openDesk login screen and no local username/password form to choose instead,
  • app access matching the persona,
  • a working <username>@<mail-domain> mailbox.

Interactive logins enforce MFA (TOTP or WebAuthn), configured as a dedicated authentik authentication flow set on the brand. The LDAP path the importer binds over is exempt, so directory sync is unaffected. Recovery is an admin operation (reset the user's authenticators), not self-service.

Passwordless login is available as well: a discoverable WebAuthn credential (a passkey) can replace password and second factor in one gesture, offered as a "use a passkey" option on the identification page. This is only sound because the stage requires user verification — the authenticator checks a biometric or PIN before signing, so the single credential still carries two factors. With user verification merely discouraged it would be a downgrade. The password + second factor path keeps working unchanged; passwordless is additive, not a replacement.

Sessions are the weak point, not authentication

Worth stating plainly, because it is easy to miss once the login itself is phishing-resistant: a session cookie is a bearer token. Whoever holds it is authenticated — no password, no passkey, no MFA prompt. Hardening authentication does nothing for the session that authentication produces.

Where that cookie lives depends on how the user opens the service, and the differences are not obvious:

How it is opened Cookie store
iOS/iPadOS web app (added to the Home Screen) separate from the browser, inside the app sandbox
macOS web app (added to the Dock) separate from Safari — cookies are copied once at creation, then independent
Installed PWA in a Chromium browser (desktop or Android) shared with that browser profile
Ordinary browser tab that browser profile

Cross-site theft is not the threat here — cookies are origin-scoped everywhere. The exposure is anything that can read the browser's cookie store: extensions, and credential-stealing malware, which routinely dumps Chromium's cookie database.

Two mitigations that are often confused:

  • A dedicated browser profile (or a dedicated browser) for the service. Extensions are installed per profile, so this removes the extension path entirely — and an installed PWA is bound to the profile it was installed from, so the launcher opens the right profile without the user choosing. It does not help against malware running as that user, which can read every profile directory. On Android there is no personal-profile equivalent.
  • Session binding. authentik's user-login stage can bind a session to network (ASN) or GeoIP (continent/country/city) information, so a cookie replayed from elsewhere is rejected. This does not prevent theft; it limits what a stolen cookie is worth.

Session binding needs a GeoIP database, and fails silently without one

authentik resolves the country or ASN from a MaxMind GeoLite2 database. If that database is not present, the binding attribute is accepted and does nothing — a security setting that appears configured while having no effect. Deploy the database (the chart ships geoipupdate sidecars, which need a free MaxMind account) and gate the binding on it, so the two cannot drift apart.

Prefer country over ASN binding if clients are mobile: the ASN modes invalidate a session when a device moves between WiFi and cellular, because that changes ASN. Country tolerates roaming inside one country and still rejects a replay from abroad, at the cost of logging out genuine travellers.

Session lifetime is the sturdiest lever

Of the mitigations above, shortening the session is the only one that needs no external dependency and no user action, and — unlike binding — it cannot be silently inert. It reduces what a stolen cookie is worth directly, rather than restricting where it can be replayed.

A user-login stage with session_duration of seconds=0 ends the session when the browser closes, and a "remember me" offset applies only when the user opts in, so a long-lived cookie stays a deliberate choice rather than the default. Keep the offset short — days, not weeks. A month-long offset means a cookie copied once is useful for a month, against an adversary that authentication hardening cannot touch; a week costs a user one extra sign-in and cuts that window by roughly four.

⚠️ An idle timeout would complement this, but the user-login stage exposes no such field — only session_duration, remember_me_offset, the two binding modes and terminate_other_sessions. So a short offset is the available lever, not the complete one.

The corollary is uncomfortable and worth stating: short sessions plus passkeys beat long sessions plus an isolated browser — and browser-isolation schemes routinely break WebAuthn, so the two are often mutually exclusive. If you find yourself choosing, choose the sessions.

The platform-level fix for this whole class of problem is Device Bound Session Credentials, which ties a session cryptographically to the device so an exfiltrated cookie is useless. At the time of writing it is in development in Chrome (desktop only) and needs server-side support, so it is a direction rather than an option.

Names & mail

  • authentik has a single display-name field. A proper first/last split is stored in the user's attributes (givenName/sn) and served over LDAP, so downstream systems receive structured names.
  • The mailbox primary address is owned by the openDesk directory: derived as <username>@<mail-domain> and only ever set when empty, never overwriting an existing address. Renaming a mailbox is a deliberate operation (set the new primary, keep the old as an alias so it still receives) — not an automatic effect of a name change.

For adopters

Enforced federated login will lock you out of openDesk administration unless you prepare first

Redirecting every login to the upstream IdP (openDesk's functional.authentication.ssoFederation.enforceFederatedLogin) also removes the local login, and the built-in Administrator is a local account. The redirect is unconditional, and the usual Keycloak escape hatch (an empty kc_idp_hint) does not survive: both the account-console and the portal strip it. So before enabling it:

  1. Create a federated administrative account and give it the directory's admin group, then log in with it once while the local form still exists, so the identity is linked.
  2. Check your IdP's application-access gate. If access to the openDesk application is restricted to your persona/role groups, an administrative account that deliberately has no persona is denied the application — the login fails at the IdP before openDesk is reached. It needs a group that grants the application without being a persona.
  3. Expect the administrative account to be groupware-free: directory admin groups are normally excluded from mail and app access, so do not put admin rights on a daily-use account — it cripples it. Create the account from the vendor's admin user template, which sets the "no groupware" attributes for you.

Reverting is supported (set the value back to empty) but it is not instant: it needs a regeneration and a re-run of the realm bootstrap, so treat it as a planned change.

Applying a realm-configuration change needs two steps

openDesk's realm settings are applied by a bootstrap Job whose pod-template checksum does not cover the realm configuration. A configuration-only change therefore does not re-run it: delete the bootstrap Job and re-sync, or the realm silently keeps its previous configuration. A regeneration also rolls the groupware middleware as collateral (a broad config checksum), and that component can return to 1/1 Ready while still 404ing its API — verify the API answers rather than trusting pod readiness.

  • This requires an upstream IdP that exposes an LDAP outpost (here: authentik). Without one, disable the importer (global.components.directoryImporter.enabled: false) and manage openDesk users natively — see Component toggles.
  • The importer (identity) and reconciler (access + baseline mail) are configured in the directory-importer chart values — the source/target directories, the persona gate groups, the app-<group> → attribute map, and the baseline-mail settings.

List only the properties you actually sync

A Nubus/UDM directory-importer substitutes the directory template default for any property you list but the source doesn't supply for a given user. So listing a property like the primary mail address or the object identifier in the importer's property map — when the source has no value for it — silently resets that field to its default on every user, which can break mail routing or identity continuity. Keep the importer's property list to the bare identity fields it genuinely carries (username, first/last name); let the access/mail reconciler own everything else.

authentik re-imports blueprints only on worker startup

Editing an authentik blueprint (groups, authorization, MFA flow) and syncing the ConfigMap is not enough — authentik only re-imports blueprints when its worker starts or rediscovers. Roll the authentik worker deployment after a blueprint change for it to take effect.