Three verification spikes before the Kerberos-authoritative build (M1) #301
Labels
No labels
bug
enhancement
in-progress
needs-info
needs-triage
p0
ready-for-agent
ready-for-human
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/nixos#301
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
M1 of #299. Three cheap questions whose answers change what gets built. All human-run; none needs a PR.
1. Does Dovecot pass PAM the bare local part or the full address?
Decides whether the Kerberos passdb is
driver = pamwith pam_krb5 (simple, gets keytab verification free) or acheckpasswordscript (~30 lines, full control of the mapping).auth_username_formatis unset inhosts/mail/modules/, so the 2.3 default%Luapplies — full lowercaseduser@domain— and it must stay that way becausemail_locationuses%d/%nand every LDAP filter keys onmail=%u. If PAM receives the full address, pam_krb5 formsdominik.polakovics@cloonar.com@CLOONAR.COMand (b) is dead. Giving Dovecot its own principal name is not an escape: one human must have exactly one principal, or the drift is back.Safe test — put the PAM passdb last, so successful logins stop at LDAP and never reach it, and
pam_denymeans it cannot authenticate anyone regardless.mailrebuilds from/var/bentoevery five minutes (utils/bento.nix:6,10), so stopbento-upgrade.timerfirst and let it restore the config afterwards.Then
doveadm auth test dominik.polakovics@cloonar.com definitely-wrongand readjournalctl -t dovecot-pamtest. LDAP rejects the wrong password,result_failuredefaults tocontinue, and the chain falls through to PAM.Also worth running once with a correct password for an account on another domain: it should succeed at the LDAP passdb and log nothing, confirming the chain order before anything real is built on it.
2. Can SDDM render an expired-password prompt?
The admin-reset flow is
kadmin cpwwith the password marked expired, so the user is forced to set their own at next login. That is a PAMCHANGE_EXPIRED_AUTHTOKconversation delivered by the greeter. GDM handles it; SDDM has a patchy history.kadmin.local -q "cpw -e now dominik.polakovics", then log in at the greeter. If it prompts, the flow works. If it just says login failed, the user has no way forward and the design needs a different greeter or unexpired temporary passwords.3. Does
useKerberosForPasswordAuthenticationfall back to an LDAP bind?It should not — the documented behaviour is that it replaces LDAP password authentication. Confirm it, because the sequencing depends on it: if there is no fallback, then flipping it locks out every federated user who has no principal yet, and M4 cannot precede M6.
Test with a user that has an LDAP password and no principal. Being certain matters more than the answer.
All three spikes run — results
Run 2026-08-08. Two of the three came back differently from what the issue predicted, and both change the build.
Spike 1 — Dovecot hands PAM the full address
Measured on a throwaway Dovecot instance under its own
base_dir; production Dovecot was never reconfigured, restarted, or contacted.Confirmed independently by
auth_debug:pam(dominik.polakovics@cloonar.com): lookup service=dovecot-pamtest. Host runs Dovecot 2.3.21.1 (pkgs.dovecot_2_3, pinned bystateVersion 22.11 < 26.05). The second-domain result shows the format is not tenant-dependent.→ M3 is a
checkpasswordscript. The pam_krb5 route is dead. It would formdominik.polakovics@cloonar.com@CLOONAR.COM, which cannot resolve. There is no escape:auth_username_formatmust stay%Lu(mail_locationuses%d/%n, filters key onmail=%u, and the primary passdb'sbase = ou=users,dc=%Dd), and 2.3's PAM passdb has no per-passdb username rewriting — its documented args aresession,setcred,max_requests,failure_show_msg,cache_key,%s,override_fields. That only became per-passdb in 2.4.Two things for whoever builds M3:
result_failure = continuecarries them to the LDAP passdbs untouched.kinitcheck trusts whatever answers as the KDC. The script must verify against a keytab or the exposure must be accepted deliberately. Onmailthe KDC is local so the real risk is near nil, but ADR-0023 sells this as a reference architecture and a split deployment inherits it.Spike 2 — SDDM renders the prompt; two bugs found and fixed
The greeter question is answered: yes. SDDM renders
CHANGE_EXPIRED_AUTHTOKcorrectly. The "different greeter" contingency is dead, and so is the theme suspicion — the minimalwhere-is-my-sddm-themehandles it.Getting there required fixing two real defects:
#305 — kpasswd (464) was firewalled.
kadmindlistened on 464 and 749, butnixos-fwallowed only 88. SSSD'schpass_provider = krb5with nokrb5_kpasswdset assumes the service runs on the KDC, so every change aimed at a dropped port. The failure was silent: the greeter rendered the prompt, accepted a new password, reported nothing, and logged the user in from SSSD's offline cache (krb5_store_password_if_offline = true,ldap-login.nix:52) — while the principal keptREQUIRES_PWCHANGEand its old key.Under ADR-0023, where Kerberos is the only credential, that is a user who believes they rotated a compromised password and did not. M5's Required Action must handle a failed kpasswd explicitly rather than trusting a clean return.
#306 — the password stack prompted twice. Old password ×2, new ×4.
pam_chauthtok()runs the stack in two passes (PAM_PRELIM_CHECK,PAM_UPDATE_AUTHTOK) and bothpam_unix(10200) andpam_sss(10900) prompted in each, becausepam_ssscarries nouse_authtok.pam_unixcan never succeed for these accounts — they are declared locally with no password on purpose (ldap-login.nix:120-122) so SDDM lists them. Fixed by orderingpam_sssfirst so it short-circuits assufficient.Not a correctness bug: kvno stepped 4 → 5, not 4 → 6, across a full doubled sequence.
Correction to this issue's setup text:
kadmin.local -q "cpw -e now <princ>"does not work.-eoncpwis the keysalt list. Usecpw -pw <temp>thenmodprinc +needchange.Spike 3 — no fallback
With
useKerberosForPasswordAuthentication = trueon thecloonarrealm:dominik.polakovics(has principal)dominik.polakovics(has principal)office@cloonar.com(no principal)It replaces LDAP password authentication entirely. The documented behaviour holds.
Getting a clean answer needed two prerequisites nobody had identified — see below.
Two findings nobody was looking for
web-armhad never been able to reach the KDC.keycloak/default.nix:153-156states the assumption outright — "Accepting a ticket needs no KDC contact, so only the realm mapping matters." True for SPNEGO, which validates against the keytab locally.realms."CLOONAR.COM".kdcwas configured but never exercised.useKerberosForPasswordAuthenticationis the first thing that uses it. M4 has an unstated prerequisite: a firewall rule permitting web-arm → mail:88, which lives in the Hetzner console, outside git. That comment is now misleading and should be corrected.The dual-stack v6 trap from #97 bit a second consumer.
mail.cloonar.comis dual-stack (ADR-0016) andweb-armhas v6 egress (ADR-0010), so RFC 6724 makes clients prefer2a01:4f8:c012:9d85::2. With only an IPv4 firewall rule,kinitworked (MIT krb5 iterates all addresses and falls back) while Keycloak hung — the JVM's Kerberos client has no v6→v4 fallback, exactly as FreeScout's IMAP client had none in #97. Resolved by opening the v6 source.Worth noting the residual: the JVM will hang the same way any time the v6 path breaks, while
kiniton the same host keeps working. ADR-0016's precedent for this class was to pin the consumer to IPv4. Also, the current rules are TCP-only on 88/464 — Kerberos tries UDP first and falls back to TCP for oversized responses, so this works until a response outgrows a datagram.Consequence: the milestone order is wrong
Spike 3 does more than reorder M4 and M6 — it shows no ordering of them as written works:
office@cloonar.com.userPassword. Keycloak is still binding to LDAP at that point, so each user loses web SSO the moment they are enrolled.useKerberosForPasswordAuthenticationis a realm-level flag and enrolment is explicitly per-user. A per-user cutover cannot be expressed through a fleet-wide switch.Tracked as a design question in the follow-up issue rather than here. #299's milestone list has been corrected.
Smaller follow-ups
/etc/pam.d/passwdonnblikely has the sameunix→sssordering #306 fixed forlogin. Left alone deliberately: setting.orderon a service whosesssrule does not exist would create a rule with nomodulePath.ldap-login.nix:21setsconfig_file_version = 2, which SSSD 2.13 rejects — log noise only.kerberos.nix:5still describes the pre-#305 Hetzner boundary.ADR-0023is ambiguous in-tree:turn-runner.nix:19andapi.reptide.eu.nix:28use the bare number for fit's ADR-0023, and this repo now has its own.Maximum ticket life: 1 day/Maximum renewable life: 0against ADR-0023 decision 9's10h/7d.kdc.confrealm defaults do not apply retroactively, so enrolment must set these per principal. Recorded on #299 so it is not lost.All three spikes are answered. Closing.