M2: SSSD LDAP identity + Kerberos login on nb (coexisting with local dominik) #269

Closed
opened 2026-08-07 12:06:29 +02:00 by dominik.polakovics · 1 comment

Part of #266 (M2 of M1–M6). Follows #267, which is complete: the KDC on mail is live, dominik.polakovics@CLOONAR.COM exists, and kinit returns a TGT.

Give nb an SSSD-backed LDAP identity whose login yields a Kerberos TGT, coexisting with the existing local dominik account rather than replacing it. Design context is in docs/adr/0022-keycloak-and-scoped-kerberos.md.

The coexistence rule — this is the whole shape of the issue

users.users.dominik stays exactly as it is: uid 1000, /home/dominik, its hashedPassword, the ~570-line hosts/nb/users/dominik.nix home-manager config, and all four sops.secrets.*.owner = "dominik" declarations (users/dominik.nix:171,174, modules/desktop/rustdesk.nix:41, modules/mcp-global.nix:79). The LDAP account dominik.polakovics (uid 5001, gid 5001, home /home/dominik.polakovics) lands alongside it.

The rename, the uid migration and the chown sweep under /nix/persist/home are deliberately deferred until SPNEGO is proven at M3. Do not start them here.

Safety — nb is the maintainer's daily laptop

nb runs nixos-rebuild switch unattended via bento every 5 minutes. A broken PAM stack locks the user out of their own machine.

  • The change must be purely additive. Local dominik authenticating against pam_unix with its existing hashedPassword must keep working with SSSD stopped, misconfigured, or the network down.
  • Do not make pam_sss required anywhere. NixOS orders it sufficient ahead of pam_unix; keep it that way so any SSSD failure falls through to local auth.
  • Do not touch security.pam.services.* entries that already exist beyond adding makeHomeDir where noted.

Scope

Create hosts/nb/modules/ldap-login.nix (new) and import it from hosts/nb/configuration.nix.

Do not reuse utils/modules/ldap-auth.nix

It is imported by web-arm (hosts/web-arm/configuration.nix:29), so editing it puts a production host in the blast radius of a laptop change. It is also wrong for nb in three ways:

  • It sets services.openssh.settings.AuthorizedKeysCommand + AuthorizedKeysCommandUser = "nslcd" and creates an nslcd user (ldap-auth.nix:59-77) — unwanted on a laptop.
  • It uses auth_provider = ldap; nb needs krb5.
  • It has no krb5, KCM, or offline-cache configuration at all.

Write a standalone module. Leave utils/modules/ldap-auth.nix untouched in this PR.

SSSD configuration

[sssd]
config_file_version = 2
services = nss, pam
domains = cloonar.com

[domain/cloonar.com]
id_provider   = ldap
auth_provider = krb5
chpass_provider = krb5

ldap_uri              = ldap://ldap.cloonar.com
ldap_id_use_start_tls = true
ldap_search_base      = dc=cloonar,dc=com
ldap_user_search_base = ou=users,dc=cloonar,dc=com
ldap_default_bind_dn  = cn=linuxbind,ou=system,ou=users,dc=cloonar,dc=com
ldap_default_authtok  = $SSSD_LDAP_DEFAULT_AUTHTOK

krb5_realm  = CLOONAR.COM
krb5_server = mail.cloonar.com
krb5_store_password_if_no_ccache = true

cache_credentials = true
use_fully_qualified_names = False
  • services.sssd.kcm = true so tickets land in the kernel keyring-backed KCM cache rather than a file, which is what survives a desktop session cleanly.
  • cache_credentials = true is required: nb roams, and the KDC is reachable only from fw and over WireGuard. Off-VPN the user must still be able to log in from cache (with no TGT — that is expected, see ADR-0022 Consequences).
  • The bind password comes from the already-committed sssd-environment secret in hosts/nb/secrets.yaml. Declare sops.secrets.sssd-environment = { }; and wire services.sssd.environmentFile. Same pattern as utils/modules/ldap-auth.nix:48.
  • security.krb5 client config on nb mirroring hosts/mail/modules/kerberos.nix (realm CLOONAR.COM, kdc/admin_server mail.cloonar.com, dns_lookup_realm/dns_lookup_kdc false, domain_realm for cloonar.com and .cloonar.com).

Group schema — verify, do not copy blindly. utils/modules/ldap-auth.nix:42-43 sets ldap_schema = rfc2307 + ldap_group_member = memberUid, while hosts/web-arm/modules/authelia.nix:77 filters groups as (&(member={dn})(objectClass=groupOfNames)) — those two descriptions of the directory disagree. Whichever is actually correct, the identity lookup demonstrably works on web-arm today, so mirror what works there for the id side and flag the group question in a comment rather than guessing. Supplementary-group resolution is a verification item below, not a blocker for login.

PAM and home directory

  • security.pam.services.sddm.makeHomeDir = true. utils/modules/ldap-auth.nix:51-54 covers login, sshd, sudo and systemd-user but not sddm, which is nb's actual display manager (hosts/nb/modules/desktop/default.nix:98). Without it the first graphical login has no home directory.
  • /home is already persisted (hosts/nb/configuration.nix:169-174, directories = [ "/home" ]), so /home/dominik.polakovics survives reboots with no extra impermanence entry. Confirm, don't add a duplicate.
  • Set enumerate = true so SDDM lists the account in its user picker (showUsersByDefault = true in the theme). Upstream discourages enumeration on large directories; this one is small and web-arm already does it. If it turns out SDDM still won't show the account, typing the username manually is an acceptable fallback — note it rather than fighting it.

Browser SPNEGO policy

The dominik.polakovics session has no home-manager, so the policy must be system-level.

Firefox needs network.negotiate-auth.trusted-uris = .cloonar.com, expressed as an enterprise policy (Authentication.SPNEGO). Note the wrinkle: nb installs Firefox via environment.systemPackages (hosts/nb/modules/desktop/default.nix:36), not programs.firefox.enable, and programs.firefox.policies requires the latter. Prefer dropping environment.etc."firefox/policies/policies.json" so nothing about the existing Firefox installation changes; switching to programs.firefox.enable is acceptable only if the systemPackages entry is removed in the same change to avoid two Firefoxes in the closure. Verify which approach the installed build actually honours.

Chromium/Chrome AuthServerAllowlist is optional — add it only if a Chromium-family browser is already in nb's package set.

Out of scope

  • Any change to users.users.dominik, its uid, its home, or its home-manager config.
  • The chown sweep / uid migration / account rename (deferred past M3).
  • utils/modules/ldap-auth.nix and anything on web-arm or mail.
  • home-manager for dominik.polakovics; the test session is deliberately a bare desktop.
  • wheel/sudo for the LDAP account — it should not have it.
  • GSSAPI SSH, GSSAPI Dovecot, Keycloak, oauth2-proxy.

ADR

docs/adr/0022-keycloak-and-scoped-kerberos.md already covers M2. Amend it only if something here contradicts it — in which case say so explicitly rather than silently diverging.

Verification

  • scripts/pre-commit dry-builds nb. Do not run scripts/test-configuration manually.
  • The agent cannot reach the fleet; runtime checks below are the human's.

Human, after deploy:

  1. getent passwd dominik.polakovics → uid 5001, home /home/dominik.polakovics.
  2. getent group 5001 resolves.
  3. id dominik.polakovics — note whether supplementary LDAP groups appear (this is the rfc2307-vs-groupOfNames question above; primary group is what matters for login).
  4. Confirm local dominik still logs in with its own password before logging out.
  5. Log into the dominik.polakovics SDDM entry using the Kerberos password set at #267.
  6. In that session: klist shows krbtgt/CLOONAR.COM@CLOONAR.COM; echo $KRB5CCNAME shows a KCM cache.
  7. Offline check: drop the WireGuard tunnel, log out, log back in — login should succeed from cache with no TGT.

Definition of done

  • hosts/nb/modules/ldap-login.nix exists and is imported; nb dry-builds clean.
  • No diff to users.users.dominik, hosts/nb/users/dominik.nix, utils/modules/ldap-auth.nix, or any host other than nb.
  • Local dominik login is provably unaffected by inspection of the PAM ordering.
  • PR opened with Closes #<this issue>.
Part of #266 (M2 of M1–M6). Follows #267, which is complete: the KDC on `mail` is live, `dominik.polakovics@CLOONAR.COM` exists, and `kinit` returns a TGT. Give `nb` an SSSD-backed LDAP identity whose login yields a Kerberos TGT, **coexisting with the existing local `dominik` account** rather than replacing it. Design context is in `docs/adr/0022-keycloak-and-scoped-kerberos.md`. ## The coexistence rule — this is the whole shape of the issue `users.users.dominik` stays **exactly as it is**: uid 1000, `/home/dominik`, its `hashedPassword`, the ~570-line `hosts/nb/users/dominik.nix` home-manager config, and all four `sops.secrets.*.owner = "dominik"` declarations (`users/dominik.nix:171,174`, `modules/desktop/rustdesk.nix:41`, `modules/mcp-global.nix:79`). The LDAP account `dominik.polakovics` (uid 5001, gid 5001, home `/home/dominik.polakovics`) lands **alongside** it. The rename, the uid migration and the `chown` sweep under `/nix/persist/home` are deliberately deferred until SPNEGO is proven at M3. Do not start them here. ## Safety — `nb` is the maintainer's daily laptop `nb` runs `nixos-rebuild switch` unattended via bento every 5 minutes. A broken PAM stack locks the user out of their own machine. - The change must be **purely additive**. Local `dominik` authenticating against `pam_unix` with its existing `hashedPassword` must keep working with SSSD stopped, misconfigured, or the network down. - Do not make `pam_sss` required anywhere. NixOS orders it `sufficient` ahead of `pam_unix`; keep it that way so any SSSD failure falls through to local auth. - Do not touch `security.pam.services.*` entries that already exist beyond adding `makeHomeDir` where noted. ## Scope Create **`hosts/nb/modules/ldap-login.nix`** (new) and import it from `hosts/nb/configuration.nix`. ### Do not reuse `utils/modules/ldap-auth.nix` It is imported by `web-arm` (`hosts/web-arm/configuration.nix:29`), so editing it puts a production host in the blast radius of a laptop change. It is also wrong for `nb` in three ways: - It sets `services.openssh.settings.AuthorizedKeysCommand` + `AuthorizedKeysCommandUser = "nslcd"` and creates an `nslcd` user (`ldap-auth.nix:59-77`) — unwanted on a laptop. - It uses `auth_provider = ldap`; `nb` needs `krb5`. - It has no krb5, KCM, or offline-cache configuration at all. Write a standalone module. **Leave `utils/modules/ldap-auth.nix` untouched in this PR.** ### SSSD configuration ``` [sssd] config_file_version = 2 services = nss, pam domains = cloonar.com [domain/cloonar.com] id_provider = ldap auth_provider = krb5 chpass_provider = krb5 ldap_uri = ldap://ldap.cloonar.com ldap_id_use_start_tls = true ldap_search_base = dc=cloonar,dc=com ldap_user_search_base = ou=users,dc=cloonar,dc=com ldap_default_bind_dn = cn=linuxbind,ou=system,ou=users,dc=cloonar,dc=com ldap_default_authtok = $SSSD_LDAP_DEFAULT_AUTHTOK krb5_realm = CLOONAR.COM krb5_server = mail.cloonar.com krb5_store_password_if_no_ccache = true cache_credentials = true use_fully_qualified_names = False ``` - `services.sssd.kcm = true` so tickets land in the kernel keyring-backed KCM cache rather than a file, which is what survives a desktop session cleanly. - `cache_credentials = true` is required: `nb` roams, and the KDC is reachable only from `fw` and over WireGuard. Off-VPN the user must still be able to log in from cache (with no TGT — that is expected, see ADR-0022 Consequences). - The bind password comes from the already-committed `sssd-environment` secret in `hosts/nb/secrets.yaml`. Declare `sops.secrets.sssd-environment = { };` and wire `services.sssd.environmentFile`. Same pattern as `utils/modules/ldap-auth.nix:48`. - `security.krb5` client config on `nb` mirroring `hosts/mail/modules/kerberos.nix` (realm `CLOONAR.COM`, kdc/admin_server `mail.cloonar.com`, `dns_lookup_realm`/`dns_lookup_kdc` false, `domain_realm` for `cloonar.com` and `.cloonar.com`). **Group schema — verify, do not copy blindly.** `utils/modules/ldap-auth.nix:42-43` sets `ldap_schema = rfc2307` + `ldap_group_member = memberUid`, while `hosts/web-arm/modules/authelia.nix:77` filters groups as `(&(member={dn})(objectClass=groupOfNames))` — those two descriptions of the directory disagree. Whichever is actually correct, the identity lookup demonstrably works on `web-arm` today, so mirror what works there for the id side and flag the group question in a comment rather than guessing. Supplementary-group resolution is a verification item below, not a blocker for login. ### PAM and home directory - `security.pam.services.sddm.makeHomeDir = true`. `utils/modules/ldap-auth.nix:51-54` covers `login`, `sshd`, `sudo` and `systemd-user` but **not** `sddm`, which is `nb`'s actual display manager (`hosts/nb/modules/desktop/default.nix:98`). Without it the first graphical login has no home directory. - `/home` is already persisted (`hosts/nb/configuration.nix:169-174`, `directories = [ "/home" ]`), so `/home/dominik.polakovics` survives reboots with no extra impermanence entry. Confirm, don't add a duplicate. - Set `enumerate = true` so SDDM lists the account in its user picker (`showUsersByDefault = true` in the theme). Upstream discourages enumeration on large directories; this one is small and `web-arm` already does it. If it turns out SDDM still won't show the account, typing the username manually is an acceptable fallback — note it rather than fighting it. ### Browser SPNEGO policy The `dominik.polakovics` session has **no home-manager**, so the policy must be system-level. Firefox needs `network.negotiate-auth.trusted-uris = .cloonar.com`, expressed as an enterprise policy (`Authentication.SPNEGO`). Note the wrinkle: `nb` installs Firefox via `environment.systemPackages` (`hosts/nb/modules/desktop/default.nix:36`), not `programs.firefox.enable`, and `programs.firefox.policies` requires the latter. Prefer dropping `environment.etc."firefox/policies/policies.json"` so nothing about the existing Firefox installation changes; switching to `programs.firefox.enable` is acceptable only if the systemPackages entry is removed in the same change to avoid two Firefoxes in the closure. Verify which approach the installed build actually honours. Chromium/Chrome `AuthServerAllowlist` is optional — add it only if a Chromium-family browser is already in `nb`'s package set. ## Out of scope - Any change to `users.users.dominik`, its uid, its home, or its home-manager config. - The `chown` sweep / uid migration / account rename (deferred past M3). - `utils/modules/ldap-auth.nix` and anything on `web-arm` or `mail`. - home-manager for `dominik.polakovics`; the test session is deliberately a bare desktop. - `wheel`/sudo for the LDAP account — it should not have it. - GSSAPI SSH, GSSAPI Dovecot, Keycloak, oauth2-proxy. ## ADR `docs/adr/0022-keycloak-and-scoped-kerberos.md` already covers M2. Amend it only if something here contradicts it — in which case say so explicitly rather than silently diverging. ## Verification - `scripts/pre-commit` dry-builds `nb`. Do not run `scripts/test-configuration` manually. - The agent cannot reach the fleet; runtime checks below are the human's. Human, after deploy: 1. `getent passwd dominik.polakovics` → uid 5001, home `/home/dominik.polakovics`. 2. `getent group 5001` resolves. 3. `id dominik.polakovics` — note whether supplementary LDAP groups appear (this is the `rfc2307`-vs-`groupOfNames` question above; primary group is what matters for login). 4. **Confirm local `dominik` still logs in with its own password before logging out.** 5. Log into the `dominik.polakovics` SDDM entry using the **Kerberos** password set at #267. 6. In that session: `klist` shows `krbtgt/CLOONAR.COM@CLOONAR.COM`; `echo $KRB5CCNAME` shows a KCM cache. 7. Offline check: drop the WireGuard tunnel, log out, log back in — login should succeed from cache with **no** TGT. ## Definition of done - `hosts/nb/modules/ldap-login.nix` exists and is imported; `nb` dry-builds clean. - No diff to `users.users.dominik`, `hosts/nb/users/dominik.nix`, `utils/modules/ldap-auth.nix`, or any host other than `nb`. - Local `dominik` login is provably unaffected by inspection of the PAM ordering. - PR opened with `Closes #<this issue>`.
Author
Owner

Heads-up for M3, since this issue is the spec a follow-on will copy from: the sssd.conf block above has krb5_store_password_if_no_ccache = true, which is not an SSSD option.

It is absent from sssd 2.13's src/config/cfg_rules.ini, and sss_ini_call_validators()'s return value is discarded in monitor.c — so the daemon starts, logs Attribute 'krb5_store_password_if_no_ccache' is not allowed in section 'domain/cloonar.com'. Check for typos., and silently ignores the line. The offline→online TGT behaviour it was meant to give would never have happened.

The real option is krb5_store_password_if_offline (see sssd-krb5(5)), which is what #270 uses. Note the semantics differ slightly from the issue's comment: it is about caching the password while the provider is offline and requesting a TGT once the KDC is reachable again, not about "there is no ccache to put a ticket in yet".

#270 also diverges on two other points where the spec could not work as written — security.pam.services.sddm.makeHomeDir renders no PAM rule (SDDM sets useDefaultRules = false), and enumerate = true cannot surface in SDDM's picker on NixOS (NSS modules load only inside nsncd; the nscd protocol has no enumeration request, and SDDM uses getpwent()). Both are explained in-file and in the PR description.

Heads-up for M3, since this issue is the spec a follow-on will copy from: the `sssd.conf` block above has `krb5_store_password_if_no_ccache = true`, which is **not an SSSD option**. It is absent from sssd 2.13's `src/config/cfg_rules.ini`, and `sss_ini_call_validators()`'s return value is discarded in `monitor.c` — so the daemon starts, logs `Attribute 'krb5_store_password_if_no_ccache' is not allowed in section 'domain/cloonar.com'. Check for typos.`, and silently ignores the line. The offline→online TGT behaviour it was meant to give would never have happened. The real option is `krb5_store_password_if_offline` (see `sssd-krb5(5)`), which is what #270 uses. Note the semantics differ slightly from the issue's comment: it is about caching the password while the provider is offline and requesting a TGT once the KDC is reachable again, not about "there is no ccache to put a ticket in yet". #270 also diverges on two other points where the spec could not work as written — `security.pam.services.sddm.makeHomeDir` renders no PAM rule (SDDM sets `useDefaultRules = false`), and `enumerate = true` cannot surface in SDDM's picker on NixOS (NSS modules load only inside nsncd; the nscd protocol has no enumeration request, and SDDM uses `getpwent()`). Both are explained in-file and in the PR description.
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#269
No description provided.