Real desktop SSO: replace Authelia with Keycloak + a Kerberos KDC #266

Open
opened 2026-08-07 01:08:19 +02:00 by dominik.polakovics · 1 comment

Goal

Real single sign-on for nb: log into the notebook with the LDAP account, and be logged into the web services as a consequence — no second password prompt at the IdP.

This requires the desktop login to leave behind a credential the browser can present silently. The only mature open-source mechanism for that is Kerberos + SPNEGO.

Why this means replacing Authelia

  • Authelia has no Kerberos/SPNEGO. The implementation PR (authelia#10976) is an open draft, last touched Dec 2025, with maintainers objecting to the gokrb5 dependency ("not maintained… uses a fork of go's X repositories which have known CVEs"). Not landing on any plannable horizon.
  • Authelia also has no Relying Party role, so it cannot federate to an upstream IdP that does speak Kerberos. Their docs are explicit: "We do not intend to support this functionality at this moment in time."

So SPNEGO means swapping the IdP.

Proposed shape

  1. KDC on mail — standalone Heimdal/MIT via services.kerberos_server (in nixpkgs at services/system/kerberos/default.nix).
  2. Keep OpenLDAP. Bridge the password stores by making Kerberos authoritative: userPassword: {SASL}user@REALM, slapd delegating binds to the KDC via saslauthd. nixpkgs' openldap is already built with --enable-spasswd and links cyrus_sasl, so this works without a package override.
  3. Keycloak on web-arm replacing Authelia as the OIDC provider — SPNEGO via LDAP user federation + keytab.
  4. oauth2-proxy to cover the *.cloonar.com forward-auth catch-all, which Keycloak has no equivalent for.
  5. SSSD on nb (services.sssd, id_provider = ldap + auth_provider = krb5, kcm = true) so SDDM login yields a TGT, with offline credential caching for a laptop.
  6. Browser policy: Firefox network.negotiate-auth.trusted-uris = .cloonar.com, Chrome AuthServerAllowlist.

Bonus from the same Kerberos infrastructure: GSSAPI SSO to Dovecot (IMAP/SMTP in Thunderbird) and across the fleet over SSH.

Why Keycloak over authentik

authentik is the better structural fit — its proxy provider maps directly onto the existing nginx auth_request setup, no oauth2-proxy needed. It was the initial lean. Resource and packaging constraints reverse it:

Marginal RAM NixOS packaging Build cost on arm64
Keycloak ~0.7–1.0 GB services.keycloak in nixpkgs repackaged JVM distribution, no real compile
authentik ~1.0–1.3 GB flake only (nixpkgs PR #375509 still open) source build: Node frontend + Go outposts + Python

web-arm has no substituters or cachix configured and nixos-rebuilds itself via bento every 5 min, so an authentik version bump means a full source build on a small ARM VM every time. That's the decider.

Why not FreeIPA

Considered and rejected:

  • FreeIPA has no OIDC provider. Its external IdP support runs the opposite direction — FreeIPA is the OAuth2 relying party, and their own docs recommend Keycloak as the IdP behind it. It would not remove the need for Keycloak; it's additive.
  • FreeIPA server is not in nixpkgs — client only (#265754, stale). Would run as a Fedora/RHEL container, outside the bento/SFTP deploy pipeline.
  • Single-realm/single-suffix, incompatible with the deliberately multi-tenant OpenLDAP on mail (see CONTEXT.md).
  • ~2–4 GB RAM, and its ARM page notes Dogtag CA startup problems on aarch64 — "not uncommon for the CA to take 10 minutes or more."

Cost to weigh before committing

Either IdP is roughly a 10× RAM jump from Authelia — ~100 MB to ~1 GB. web-arm already carries Nextcloud, Immich, Collabora, Grafana, Prometheus, Loki, VictoriaMetrics, PowerSync, MySQL and PostgreSQL on an arm64 VM. Headroom needs checking before this is scheduled.

Migration surface: 8 OIDC clients re-registered (Gitea, Nextcloud, Grafana, FreeScout, Synapse/MAS, claude-mcp-forgejo, Immich), plus the access_control rules and the three authorization_policies in hosts/web-arm/modules/authelia.nix.

Open questions for triage

  • Actual RAM headroom on web-arm — unverified; needs free -m.
  • Do LDAP users carry posixAccount with uidNumber/gidNumber? nis.ldif is in the schema set and there's a loginShell ACL, but this isn't confirmed. It's a prerequisite for any LDAP desktop login.
  • uid migration on nb: dominik is currently a local user with a local uid. Switching to an LDAP-supplied uid needs a chown sweep over /home, which lives under impermanence at /nix/persist.
  • Is this worth it versus the cheap alternative? Authelia 4.39+ passkeys plus a TPM/fingerprint-backed FIDO2 authenticator on nb (tpm-fido2-thinkpad-linux, confirmed working with AMD TPM 2.0 + fprintd) gives one-fingerprint-touch web login for ~zero added RAM and no migration. It is not true SSO — desktop and web login stay separate events — but it does not foreclose this issue later.

Scope

Design/spike first, not a direct implementation. Suggested sequencing if it proceeds: confirm POSIX attributes and RAM headroom → KDC + {SASL} pass-through on mail (verifiable standalone with kinit) → SSSD on nb → Keycloak + oauth2-proxy on web-arm → cut over clients → retire Authelia.

An ADR is warranted for the IdP swap.

## Goal Real single sign-on for `nb`: log into the notebook with the LDAP account, and be logged into the web services as a consequence — no second password prompt at the IdP. This requires the desktop login to leave behind a credential the browser can present silently. The only mature open-source mechanism for that is **Kerberos + SPNEGO**. ## Why this means replacing Authelia - Authelia has **no Kerberos/SPNEGO**. The implementation PR ([authelia#10976](https://github.com/authelia/authelia/pull/10976)) is an open draft, last touched Dec 2025, with maintainers objecting to the `gokrb5` dependency ("not maintained… uses a fork of go's X repositories which have known CVEs"). Not landing on any plannable horizon. - Authelia also has **no Relying Party role**, so it cannot federate to an upstream IdP that does speak Kerberos. Their docs are explicit: *"We do not intend to support this functionality at this moment in time."* So SPNEGO means swapping the IdP. ## Proposed shape 1. **KDC on `mail`** — standalone Heimdal/MIT via `services.kerberos_server` (in nixpkgs at `services/system/kerberos/default.nix`). 2. **Keep OpenLDAP.** Bridge the password stores by making Kerberos authoritative: `userPassword: {SASL}user@REALM`, slapd delegating binds to the KDC via saslauthd. nixpkgs' openldap is already built with `--enable-spasswd` and links `cyrus_sasl`, so this works without a package override. 3. **Keycloak on `web-arm`** replacing Authelia as the OIDC provider — SPNEGO via LDAP user federation + keytab. 4. **oauth2-proxy** to cover the `*.cloonar.com` forward-auth catch-all, which Keycloak has no equivalent for. 5. **SSSD on `nb`** (`services.sssd`, `id_provider = ldap` + `auth_provider = krb5`, `kcm = true`) so SDDM login yields a TGT, with offline credential caching for a laptop. 6. **Browser policy**: Firefox `network.negotiate-auth.trusted-uris = .cloonar.com`, Chrome `AuthServerAllowlist`. Bonus from the same Kerberos infrastructure: GSSAPI SSO to Dovecot (IMAP/SMTP in Thunderbird) and across the fleet over SSH. ## Why Keycloak over authentik authentik is the better *structural* fit — its proxy provider maps directly onto the existing nginx `auth_request` setup, no oauth2-proxy needed. It was the initial lean. Resource and packaging constraints reverse it: | | Marginal RAM | NixOS packaging | Build cost on arm64 | |---|---|---|---| | Keycloak | ~0.7–1.0 GB | `services.keycloak` in nixpkgs | repackaged JVM distribution, no real compile | | authentik | ~1.0–1.3 GB | flake only ([nixpkgs PR #375509](https://github.com/NixOS/nixpkgs/pull/375509) still open) | source build: Node frontend + Go outposts + Python | `web-arm` has no substituters or cachix configured and `nixos-rebuild`s itself via bento every 5 min, so an authentik version bump means a full source build on a small ARM VM every time. That's the decider. ## Why not FreeIPA Considered and rejected: - **FreeIPA has no OIDC provider.** Its [external IdP support](https://freeipa.readthedocs.io/en/latest/designs/external-idp/external-idp.html) runs the *opposite* direction — FreeIPA is the OAuth2 relying party, and their own docs recommend Keycloak as the IdP behind it. It would not remove the need for Keycloak; it's additive. - **FreeIPA server is not in nixpkgs** — client only ([#265754](https://github.com/NixOS/nixpkgs/issues/265754), stale). Would run as a Fedora/RHEL container, outside the bento/SFTP deploy pipeline. - **Single-realm/single-suffix**, incompatible with the deliberately multi-tenant OpenLDAP on `mail` (see `CONTEXT.md`). - **~2–4 GB RAM**, and its [ARM page](https://www.freeipa.org/page/ARM) notes Dogtag CA startup problems on aarch64 — *"not uncommon for the CA to take 10 minutes or more."* ## Cost to weigh before committing Either IdP is roughly a **10× RAM jump from Authelia** — ~100 MB to ~1 GB. `web-arm` already carries Nextcloud, Immich, Collabora, Grafana, Prometheus, Loki, VictoriaMetrics, PowerSync, MySQL and PostgreSQL on an arm64 VM. Headroom needs checking before this is scheduled. Migration surface: 8 OIDC clients re-registered (Gitea, Nextcloud, Grafana, FreeScout, Synapse/MAS, claude-mcp-forgejo, Immich), plus the `access_control` rules and the three `authorization_policies` in `hosts/web-arm/modules/authelia.nix`. ## Open questions for triage - **Actual RAM headroom on `web-arm`** — unverified; needs `free -m`. - **Do LDAP users carry `posixAccount` with `uidNumber`/`gidNumber`?** `nis.ldif` is in the schema set and there's a `loginShell` ACL, but this isn't confirmed. It's a prerequisite for *any* LDAP desktop login. - **uid migration on `nb`**: `dominik` is currently a local user with a local uid. Switching to an LDAP-supplied uid needs a `chown` sweep over `/home`, which lives under impermanence at `/nix/persist`. - **Is this worth it versus the cheap alternative?** Authelia 4.39+ passkeys plus a TPM/fingerprint-backed FIDO2 authenticator on `nb` ([tpm-fido2-thinkpad-linux](https://github.com/mc256/tpm-fido2-thinkpad-linux), confirmed working with AMD TPM 2.0 + fprintd) gives one-fingerprint-touch web login for ~zero added RAM and no migration. It is not true SSO — desktop and web login stay separate events — but it does not foreclose this issue later. ## Scope Design/spike first, not a direct implementation. Suggested sequencing if it proceeds: confirm POSIX attributes and RAM headroom → KDC + `{SASL}` pass-through on `mail` (verifiable standalone with `kinit`) → SSSD on `nb` → Keycloak + oauth2-proxy on `web-arm` → cut over clients → retire Authelia. An ADR is warranted for the IdP swap.
Author
Owner

Design session resolved the major branches. First implementation slice filed as #267 (M1: standalone KDC on mail + the IdP-swap ADR), labelled ready-for-agent.

Two findings from that session change the plan as written in this issue:

  1. The {SASL} password bridge is rejected. hosts/mail/modules/dovecot.nix:15,23,28 uses auth_bind = no + default_pass_scheme = CRYPT — Dovecot reads the raw userPassword and verifies it itself rather than delegating a bind to slapd. Rewriting userPassword to {SASL}user@REALM would break all IMAP/SMTP auth for dc=cloonar,dc=com immediately. Kerberos is therefore additive: principals only for the humans who log into nb; LDAP stays the authoritative password store. Both GSSAPI bonuses (Dovecot/Thunderbird, fleet SSH) are additive too, so nothing is forfeited.

  2. Both open gates passed, and one of them voids an argument in this issue. posixAccount is populated (uidNumber 5001). web-arm has 10 GB available of 15.5 GB, with Authelia using 32 MB — so the RAM column in the Keycloak-vs-authentik table does not apply. Keycloak still wins, but on packaging alone (flake-only, source-builds Node+Go+Python per bump, no substituters, bento rebuilds every 5 min). Recorded honestly in the ADR, including what that choice costs: authentik's proxy provider would have removed the entire oauth2-proxy workstream at M5.

Also decided: realm CLOONAR.COM; KDC on port 88 only, Hetzner-firewalled to fw, nb via the existing WireGuard tunnel; Keycloak at sso.cloonar.com; nb gets full SSSD LDAP identity, reached by coexistence first (local dominik untouched, LDAP dominik.polakovics alongside) with the rename/uid migration deferred until SSO is proven.

Sequence is M1 KDC → M2 SSSD on nbM3 Keycloak beside Authelia (decision gate) → M4 clients → M5 oauth2-proxy → M6 retire Authelia. M3 proves SPNEGO end-to-end with production still on Authelia.

Secrets for M1–M3 are committed (cbb91be); keycloak-keytab-b64 is deliberately deferred as an M1 artifact.

Still open, needed before M4 but not blocking M1–M3: services.keycloak has no declarative realm/client management, so the auth layer's config would move out of git into a database unless keycloak-config-cli or realm-JSON import is used.

Design session resolved the major branches. First implementation slice filed as #267 (M1: standalone KDC on `mail` + the IdP-swap ADR), labelled `ready-for-agent`. Two findings from that session change the plan as written in this issue: 1. **The `{SASL}` password bridge is rejected.** `hosts/mail/modules/dovecot.nix:15,23,28` uses `auth_bind = no` + `default_pass_scheme = CRYPT` — Dovecot reads the raw `userPassword` and verifies it itself rather than delegating a bind to slapd. Rewriting `userPassword` to `{SASL}user@REALM` would break all IMAP/SMTP auth for `dc=cloonar,dc=com` immediately. Kerberos is therefore **additive**: principals only for the humans who log into `nb`; LDAP stays the authoritative password store. Both GSSAPI bonuses (Dovecot/Thunderbird, fleet SSH) are additive too, so nothing is forfeited. 2. **Both open gates passed, and one of them voids an argument in this issue.** `posixAccount` is populated (`uidNumber 5001`). `web-arm` has **10 GB available of 15.5 GB**, with Authelia using 32 MB — so the RAM column in the Keycloak-vs-authentik table does not apply. Keycloak still wins, but on packaging alone (flake-only, source-builds Node+Go+Python per bump, no substituters, bento rebuilds every 5 min). Recorded honestly in the ADR, including what that choice costs: authentik's proxy provider would have removed the entire oauth2-proxy workstream at M5. Also decided: realm `CLOONAR.COM`; KDC on port 88 only, Hetzner-firewalled to `fw`, `nb` via the existing WireGuard tunnel; Keycloak at `sso.cloonar.com`; `nb` gets full SSSD LDAP identity, reached by **coexistence first** (local `dominik` untouched, LDAP `dominik.polakovics` alongside) with the rename/uid migration deferred until SSO is proven. Sequence is M1 KDC → M2 SSSD on `nb` → **M3 Keycloak beside Authelia (decision gate)** → M4 clients → M5 oauth2-proxy → M6 retire Authelia. M3 proves SPNEGO end-to-end with production still on Authelia. Secrets for M1–M3 are committed (cbb91be); `keycloak-keytab-b64` is deliberately deferred as an M1 artifact. Still open, needed before M4 but not blocking M1–M3: `services.keycloak` has no declarative realm/client management, so the auth layer's config would move out of git into a database unless `keycloak-config-cli` or realm-JSON import is used.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#266
No description provided.