feat(mail): offer GSSAPI on dovecot IMAP #296

Merged
dominik.polakovics merged 1 commit from afk/291 into main 2026-08-08 01:08:13 +02:00

Closes #291

Enables the GSSAPI mechanism on mail's Dovecot so Thunderbird on nb can authenticate to IMAP with the desktop-login TGT. Follow-up to #290/#293, which built dovecot_2_3 with --with-gssapi=yes — that overlay is a hard prerequisite, since an unsupported mechanism in auth_mechanisms is i_fatal at auth-process start (src/auth/mech.c:162).

One file, three settings:

auth_mechanisms = [ "plain" "login" "gssapi" ];
auth_gssapi_hostname = "imap.cloonar.com";
auth_krb5_keytab = "/var/lib/dovecot/dovecot.keytab";

Before you merge — please read

1. This is not dry-built. The AFK sandbox has no Nix, so scripts/pre-commit could not run (ERROR: 'nix-instantiate' command not found) and the commit used --no-verify. Everything below was established by reading the actual nixpkgs nixos-26.05 module and dovecot 2.3.21.1 source, not by evaluating. Please let the hook run before merging.

2. SMTP and ManageSieve will start advertising GSSAPI too, and it will fail there. auth_mechanisms is global in Dovecot 2.3 — there is one mechanism register (src/auth/main.c:198-202) shared by every service, and it cannot be scoped per protocol. Postfix uses this same auth process (postfix.nix:236-241, smtpd_sasl_type = dovecot over private/auth), and smtpd_sasl_mechanism_filter is unset — its default !external, static:rest lets GSSAPI through. So 25/465/587 will announce AUTH PLAIN LOGIN GSSAPI.

Worse than a plain rejection: Dovecot builds the acceptor as <lowercased service>/<gssapi_hostname> (mech-gssapi.c:157-168), so submission asks for smtp/imap.cloonar.com, which does not exist; gss_acquire_cred fails and the request ends in auth_request_internal_failure → Postfix 454 4.7.0, a temporary failure that clients retry rather than falling back. ManageSieve on 4190 has the same problem with sieve/imap.cloonar.com. Only clients that actually select GSSAPI are affected — password clients are untouched — but that includes anything auto-negotiating the strongest mechanism.

The issue puts "any Postfix change" out of scope, so I did not make it. The one-line containment, which preserves exactly today's banner, is:

smtpd_sasl_mechanism_filter = "!external, !gssapi, static:rest";

Your call whether that belongs in this PR, a follow-up, or nowhere.

Deviations from the issue text, and why

plain login gssapi, not plain gssapi. nixpkgs 26.05 declares auth_mechanisms as an option with default [ "plain" "login" ] (nixos/modules/services/mail/dovecot.nix:442-453), so mail already advertises AUTH=LOGIN today. Writing plain gssapi would have silently withdrawn LOGIN from every client on the host. The diff strictly appends to what is served now.

Set in services.dovecot2.settings, not dovecotExtraConfig. The doveConf generator emits !include lines before the rendered settings block (dovecot.nix:170-178), and last-wins for dovecot globals. An auth_mechanisms in the include would have been overridden by the module's own default — and silently, with GSSAPI simply never offered. This is the same ordering trap the existing mail_plugins comment describes. (Type note: the option is oneOf [ (attrsOf bool) (listOf str) ], so the list form is mandatory; a bare "plain gssapi" string fails to evaluate.)

auth_gssapi_hostname added — not in the issue, but required for the stated SPN. Its default is empty, which resolves to the local hostname, so the acceptor would have been imap/mail, not imap/imap.cloonar.com. Verified that imap.cloonar.com is a real A/AAAA record and not a CNAME onto mail.cloonar.com, so krb5 forward canonicalisation is a no-op on both ends and the SPN stays put. If anyone ever converts it to a CNAME, this silently breaks.

Keytab is root:root, not dovecot. There is no dovecot user on this host — the module creates dovecot2 and dovenull, and this config's service auth { user = root } (in the include) means the auth process that reads the keytab runs as root. chown dovecot as written in the issue would have failed outright.

auth_username_format deliberately not set. Checked as the issue asked: dovecot 2.3.21.1's default is already %Lu (src/auth/auth-settings.c:297), and it is applied to the GSSAPI display name via auth_request_fix_username. dominik.polakovics@CLOONAR.COMdominik.polakovics@cloonar.com, exactly the mail attribute user_filter keys on. The mapping is free; setting it would be a no-op.

Corrected the ktadd warning. The issue's "a second run rotates the key and silently invalidates the keytab" is true for ADR-0022's Keycloak case, where the keytab is base64'd into sops and a re-run invalidates the committed copy. Here ktadd -k writes to the only copy and appends the new kvno, so the file stays valid. I left the claim out of the module rather than repeat it.

Human steps

On mail (it is the KDC, so nothing goes in sops). /var/lib/dovecot already exists — rspamd.nix's preStart creates it — so ktadd -k will not fail on a missing directory:

kadmin.local -r CLOONAR.COM
  addprinc -randkey imap/imap.cloonar.com
  ktadd -k /var/lib/dovecot/dovecot.keytab imap/imap.cloonar.com
  quit
chown root:root /var/lib/dovecot/dovecot.keytab && chmod 0400 /var/lib/dovecot/dovecot.keytab

Merging before this is safe: Dovecot registers the keytab lazily on the first GSSAPI attempt and never stats it at startup (mech-gssapi.c:106-119, called only from obtain_service_credentials behind a one-shot guard at :145-148). The daemon starts, PLAIN and LOGIN keep working, and only GSSAPI logins fail. /var/lib/dovecot is not in borgbackup.nix's excludes, so the keytab is backed up.

On nb, in the cloonar or work profile only — left as a human step per the issue's scope. Worth knowing before you do it by hand: home-manager release-25.11's thunderbird module does not emit authMethod for a non-Gmail account, so a change made in the Thunderbird UI will persist and will not be reverted by home-manager switch. If you would rather have it declarative, it is one line in the dominik.polakovics@cloonar.com account block of hosts/nb/users/dominik.nix — that account is already scoped to [ "cloonar" "work" ] and never appears in private:

settings = id: {
  # ... existing mail.identity.id_${id}.* prefs ...
  "mail.server.server_${id}.authMethod" = 5;   # GSSAPI/Kerberos
};

Note that this would cover both secondary profiles, not one, and that it locks the pref — user.js is re-applied on every Thunderbird start, so later UI changes to it in those two profiles would be reverted.

Verification

  • scripts/pre-commit dry-builds mail (not run here — no Nix in the sandbox)
  • systemctl status dovecot2 clean after deploy
  • PLAIN still works — send and receive on the four password accounts, and confirm another tenant can still log in. Check this before anything else.
  • openssl s_client -starttls smtp -connect mail.cloonar.com:587 — check whether the AUTH line now lists GSSAPI, and that a submission client on each tenant still sends (see risk 2 above)
  • On VPN with a valid TGT, the test profile authenticates with no password prompt
  • klist shows a service ticket for imap/imap.cloonar.com@CLOONAR.COM
  • Off-VPN, the test-profile account fails while the password accounts keep working (expected — Thunderbird does not fall back between auth methods)

Rollback is reverting the commit; the principal and keytab can stay or go with delprinc.

No ADR: ADR-0022 already records this decision ("they need a keytab and one more entry in auth_mechanisms"), and this is its mechanical follow-through, not a new trade-off.

Closes #291 Enables the GSSAPI mechanism on `mail`'s Dovecot so Thunderbird on `nb` can authenticate to IMAP with the desktop-login TGT. Follow-up to #290/#293, which built `dovecot_2_3` with `--with-gssapi=yes` — that overlay is a hard prerequisite, since an unsupported mechanism in `auth_mechanisms` is `i_fatal` at auth-process start (`src/auth/mech.c:162`). One file, three settings: ```nix auth_mechanisms = [ "plain" "login" "gssapi" ]; auth_gssapi_hostname = "imap.cloonar.com"; auth_krb5_keytab = "/var/lib/dovecot/dovecot.keytab"; ``` ## Before you merge — please read **1. This is not dry-built.** The AFK sandbox has no Nix, so `scripts/pre-commit` could not run (`ERROR: 'nix-instantiate' command not found`) and the commit used `--no-verify`. Everything below was established by reading the actual nixpkgs `nixos-26.05` module and dovecot 2.3.21.1 source, not by evaluating. Please let the hook run before merging. **2. SMTP and ManageSieve will start advertising GSSAPI too, and it will fail there.** `auth_mechanisms` is global in Dovecot 2.3 — there is one mechanism register (`src/auth/main.c:198-202`) shared by every service, and it cannot be scoped per protocol. Postfix uses this same auth process (`postfix.nix:236-241`, `smtpd_sasl_type = dovecot` over `private/auth`), and `smtpd_sasl_mechanism_filter` is unset — its default `!external, static:rest` lets GSSAPI through. So 25/465/587 will announce `AUTH PLAIN LOGIN GSSAPI`. Worse than a plain rejection: Dovecot builds the acceptor as `<lowercased service>/<gssapi_hostname>` (`mech-gssapi.c:157-168`), so submission asks for `smtp/imap.cloonar.com`, which does not exist; `gss_acquire_cred` fails and the request ends in `auth_request_internal_failure` → Postfix `454 4.7.0`, a *temporary* failure that clients retry rather than falling back. ManageSieve on 4190 has the same problem with `sieve/imap.cloonar.com`. Only clients that actually select GSSAPI are affected — password clients are untouched — but that includes anything auto-negotiating the strongest mechanism. The issue puts "any Postfix change" out of scope, so I did not make it. The one-line containment, which preserves exactly today's banner, is: ```nix smtpd_sasl_mechanism_filter = "!external, !gssapi, static:rest"; ``` Your call whether that belongs in this PR, a follow-up, or nowhere. ## Deviations from the issue text, and why **`plain login gssapi`, not `plain gssapi`.** nixpkgs 26.05 *declares* `auth_mechanisms` as an option with default `[ "plain" "login" ]` (`nixos/modules/services/mail/dovecot.nix:442-453`), so `mail` already advertises `AUTH=LOGIN` today. Writing `plain gssapi` would have silently withdrawn LOGIN from every client on the host. The diff strictly appends to what is served now. **Set in `services.dovecot2.settings`, not `dovecotExtraConfig`.** The `doveConf` generator emits `!include` lines *before* the rendered settings block (`dovecot.nix:170-178`), and last-wins for dovecot globals. An `auth_mechanisms` in the include would have been overridden by the module's own default — and silently, with GSSAPI simply never offered. This is the same ordering trap the existing `mail_plugins` comment describes. (Type note: the option is `oneOf [ (attrsOf bool) (listOf str) ]`, so the list form is mandatory; a bare `"plain gssapi"` string fails to evaluate.) **`auth_gssapi_hostname` added — not in the issue, but required for the stated SPN.** Its default is empty, which resolves to the local hostname, so the acceptor would have been `imap/mail`, not `imap/imap.cloonar.com`. Verified that `imap.cloonar.com` is a real A/AAAA record and not a CNAME onto `mail.cloonar.com`, so krb5 forward canonicalisation is a no-op on both ends and the SPN stays put. If anyone ever converts it to a CNAME, this silently breaks. **Keytab is `root:root`, not `dovecot`.** There is no `dovecot` user on this host — the module creates `dovecot2` and `dovenull`, and this config's `service auth { user = root }` (in the include) means the auth process that reads the keytab runs as root. `chown dovecot` as written in the issue would have failed outright. **`auth_username_format` deliberately not set.** Checked as the issue asked: dovecot 2.3.21.1's default is already `%Lu` (`src/auth/auth-settings.c:297`), and it is applied to the GSSAPI display name via `auth_request_fix_username`. `dominik.polakovics@CLOONAR.COM` → `dominik.polakovics@cloonar.com`, exactly the `mail` attribute `user_filter` keys on. The mapping is free; setting it would be a no-op. **Corrected the `ktadd` warning.** The issue's "a second run rotates the key and silently invalidates the keytab" is true for ADR-0022's Keycloak case, where the keytab is base64'd into sops and a re-run invalidates the *committed copy*. Here `ktadd -k` writes to the only copy and appends the new kvno, so the file stays valid. I left the claim out of the module rather than repeat it. ## Human steps On `mail` (it is the KDC, so nothing goes in sops). `/var/lib/dovecot` already exists — `rspamd.nix`'s preStart creates it — so `ktadd -k` will not fail on a missing directory: ``` kadmin.local -r CLOONAR.COM addprinc -randkey imap/imap.cloonar.com ktadd -k /var/lib/dovecot/dovecot.keytab imap/imap.cloonar.com quit chown root:root /var/lib/dovecot/dovecot.keytab && chmod 0400 /var/lib/dovecot/dovecot.keytab ``` Merging *before* this is safe: Dovecot registers the keytab lazily on the first GSSAPI attempt and never stats it at startup (`mech-gssapi.c:106-119`, called only from `obtain_service_credentials` behind a one-shot guard at `:145-148`). The daemon starts, PLAIN and LOGIN keep working, and only GSSAPI logins fail. `/var/lib/dovecot` is not in `borgbackup.nix`'s excludes, so the keytab is backed up. On `nb`, in the `cloonar` or `work` profile only — left as a human step per the issue's scope. Worth knowing before you do it by hand: home-manager `release-25.11`'s thunderbird module does **not** emit `authMethod` for a non-Gmail account, so a change made in the Thunderbird UI will persist and will not be reverted by `home-manager switch`. If you would rather have it declarative, it is one line in the `dominik.polakovics@cloonar.com` account block of `hosts/nb/users/dominik.nix` — that account is already scoped to `[ "cloonar" "work" ]` and never appears in `private`: ```nix settings = id: { # ... existing mail.identity.id_${id}.* prefs ... "mail.server.server_${id}.authMethod" = 5; # GSSAPI/Kerberos }; ``` Note that this would cover both secondary profiles, not one, and that it locks the pref — `user.js` is re-applied on every Thunderbird start, so later UI changes to it in those two profiles would be reverted. ## Verification - [ ] `scripts/pre-commit` dry-builds `mail` (not run here — no Nix in the sandbox) - [ ] `systemctl status dovecot2` clean after deploy - [ ] **PLAIN still works** — send and receive on the four password accounts, and confirm another tenant can still log in. Check this before anything else. - [ ] `openssl s_client -starttls smtp -connect mail.cloonar.com:587` — check whether the `AUTH` line now lists GSSAPI, and that a submission client on each tenant still sends (see risk 2 above) - [ ] On VPN with a valid TGT, the test profile authenticates with no password prompt - [ ] `klist` shows a service ticket for `imap/imap.cloonar.com@CLOONAR.COM` - [ ] Off-VPN, the test-profile account fails while the password accounts keep working (expected — Thunderbird does not fall back between auth methods) Rollback is reverting the commit; the principal and keytab can stay or go with `delprinc`. No ADR: ADR-0022 already records this decision ("they need a keytab and one more entry in `auth_mechanisms`"), and this is its mechanical follow-through, not a new trade-off.
Adds gssapi to auth_mechanisms alongside the plain and login the module
already advertises, points the acceptor at imap.cloonar.com — Thunderbird's
IMAP host, where the default would be gethostname() — and names the keytab
the human creates with kadmin.local.

Set via services.dovecot2.settings rather than dovecotExtraConfig: the 26.05
module declares auth_mechanisms itself and renders it after the !include, so
an include-side value would be silently overridden.

auth_username_format stays unset. Dovecot 2.3's default is already %Lu, which
lowercases dominik.polakovics@CLOONAR.COM to the mail attribute the LDAP
user_filter keys on, so the mapping is free.
Sign in to join this conversation.
No reviewers
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!296
No description provided.