M3 (decision gate): Keycloak at sso.cloonar.com beside Authelia, declarative realm + SPNEGO #271
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#271
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?
Part of #266 (M3 of M1–M6). Follows #267 (KDC on
mail, complete) and #269 (SSSD + Kerberos login onnb, complete — desktop login yields a TGT in KCM).M3 is the decision gate for the whole workstream. Keycloak comes up at
sso.cloonar.combeside Authelia, which keeps serving all of production untouched. The single question M3 answers is: does anbdesktop login get the user into Keycloak with no password prompt? If yes, M4–M6 proceed. If no, the workstream stops and nothing has been cut over. Design context:docs/adr/0022-keycloak-and-scoped-kerberos.md.Nothing is cut over in this issue
hosts/web-arm/modules/authelia.nixis not touched.auth.cloonar.comkeeps serving every existing client.Prerequisites already in place
keycloak-db-password,keycloak-bootstrap-admin,keycloak-keytab-b64are committed inhosts/web-arm/secrets.yaml.HTTP/sso.cloonar.com@CLOONAR.COM, kvno 2, enctypesaes256-cts-hmac-sha1-96+aes128-cts-hmac-sha1-96.cn=linuxbind,ou=system,ou=users,dc=cloonar,dc=com, whose password is already inhosts/web-arm/secrets.yamlaslinuxbind-password. Federation is READ_ONLY, so no new system DN and no LDAP write is needed.web-arm(hosts/web-arm/modules/postgresql.nix).web-armmeasured 10 GB available of 15.5 GB.sso.cloonar.comis a human step (below). It does not block the build: ACME here is DNS-01 via the Hetzner API (utils/modules/lego/lego.nix:11), so the certificate issues without an A record.Scope
Create
hosts/web-arm/modules/keycloak/(module + realm JSON) and import it fromhosts/web-arm/configuration.nix.1. The Keycloak service
services.keycloakwith the PostgreSQL backend anddatabase.passwordFile= thekeycloak-db-passwordsecret.initialAdminPasswordmust stay unset. The module renders it intoEnvironment=in the unit file, which is world-readable in the Nix store. SupplyKC_BOOTSTRAP_ADMIN_USERNAMEandKC_BOOTSTRAP_ADMIN_PASSWORDinstead viasystemd.services.keycloak.serviceConfig.EnvironmentFilepointed at thekeycloak-bootstrap-adminsecret. Leaving the option null means the module emits no competingEnvironment=lines.settings.hostnameis the full external URL. HTTP-only on loopback behind nginx, withproxy-headers = "xforwarded"so it trusts the reverse proxy'sX-Forwarded-*.2. The keytab
keycloak-keytab-b64is base64. Decode it to a stable runtime path owned by thekeycloakuser, mode 0400, in anExecStartPre.hosts/mail/modules/dovecot.nix:324is the existing in-repo pattern for rendering a secret into a runtime file at service start. Do not write it anywhere in/nix/store. The realm JSON references this path, so it must be fixed and known at build time.3. nginx vhost
sso.cloonar.comforceSSL,enableACME,acmeRoot = null— mirror the shape of the existingauth.cloonar.comvhost athosts/web-arm/modules/authelia.nix:394-397, which is the correct pattern for this host's DNS-01 setup. Reverse-proxy to Keycloak on loopback withrecommendedProxySettings.SPNEGO-specific, and the most likely thing to get silently wrong: the
Negotiatetoken travels in theAuthorizationrequest header and a Kerberos ticket can be several KB — far past nginx's default header buffers. Without enlargedlarge_client_header_buffers(and proxy buffer sizes to match), SPNEGO fails as a bare400 Bad Requestbefore it reaches Keycloak, which looks nothing like an auth problem. Size these deliberately and comment why.4. Declarative realm via
services.keycloak.realmFilesA realm JSON checked into the repo, listed in
realmFiles. Know the semantics before relying on them: the module symlinks the files into/run/keycloak/data/import/and passes--import-realm, and Keycloak skips the import entirely if the realm already exists. It is create-if-absent, not converge — running on every start but a no-op after the first. During M3 that is fine (delete the realm to re-import; nothing is in it yet). Say so in a comment so nobody later assumes editing the JSON applies changes.The realm must configure:
ldap.cloonar.com,usersDn = ou=users,dc=cloonar,dc=com, bind DNcn=linuxbind,ou=system,ou=users,dc=cloonar,dc=com,editMode = READ_ONLY, StartTLS.usernameLDAPAttributemust beuid— the LDAPuidisdominik.polakovics, which is exactly the short name of the Kerberos principal, and that correspondence is what lets Keycloak resolve a SPNEGO ticket to an LDAP user.allowKerberosAuthentication = true,kerberosRealm = CLOONAR.COM,serverPrincipal = HTTP/sso.cloonar.com@CLOONAR.COM,keyTab= the runtime path from step 2.useKerberosForPasswordAuthentication = false. This is the load-bearing scoping switch from ADR-0022 — with it off, Keycloak validates SPNEGO tickets but LDAP password logins still go touserPasswordvia an ordinary bind. Turning it on would make every LDAP login depend on the KDC, which is exactly what the ADR rejects.Kerberosexecution in the browser flow set toALTERNATIVE. A fresh realm ships itDISABLED, so SPNEGO silently never fires and everything falls through to the login form — the single most common cause of "Keycloak ignores my ticket".ALTERNATIVE(notREQUIRED) is also what preserves the fallback: users with no ticket, and every other tenant, still get the normal username/password form.5. The LDAP bind credential
It cannot go in the realm JSON —
realmFilestakes Nix store paths and those are world-readable.Preferred: Keycloak's files-plaintext vault SPI (
vault/vault-dirinservices.keycloak.settings), with the realm JSON referencing${vault.<key>}forbindCredentialand sops materialising the file. Note the vault's filename convention is<realm>_<key>, and the file must be readable by thekeycloakuser. Reuse the existinglinuxbind-passwordsecret rather than adding a new one — set itspathto land in the vault directory under the right name.If the vault SPI turns out not to be wireable through
services.keycloak.settingson this channel, fall back to entering the bind credential once in the admin console after first import, and document that clearly in the module and the PR as a known non-declarative element.ADR amendment (required)
docs/adr/0022-keycloak-and-scoped-kerberos.mdcurrently defers the declarative-vs-click-ops question to M4. That was wrong — LDAP federation and the Kerberos settings are realm configuration, so it lands at M3. Amend that section to record:realmFileschosen, its create-if-absent semantics and what they cost, how the bind credential is handled, and that convergence (e.g.keycloak-config-cli) is revisited at M4 when real clients and sessions make "delete the realm to change it" unacceptable.Out of scope
authelia.nix,auth.cloonar.com, or any existing vhost.hosts/mail/*,hosts/nb/*.Verification
scripts/pre-commitdry-buildsweb-arm. Do not runscripts/test-configurationmanually.Human, after deploy:
sso.cloonar.com→web-arm. The host must match the SPN exactly; the browser derivesHTTP/sso.cloonar.comfrom the URL.systemctl status keycloakonweb-arm; confirm the realm imported and the keytab decoded to the expected path withkeycloakownership.auth.cloonar.comand an existing client (e.g. Grafana) still work — Authelia must be unaffected.dominik.polakovicssession onnb, on the VPN, withklistshowing a TGT: browse tohttps://sso.cloonar.com/realms/<realm>/account.ALTERNATIVE, then nginx header buffer sizes, then keytab enctypes/kvno.dominiksession), the same URL must show the normal login form and accept the LDAP password — provinguseKerberosForPasswordAuthentication = falseleft ordinary auth intact.Definition of done
sso.cloonar.com, realm imported declaratively from the repo, SPNEGO configured.authelia.nixor any existing vhost;web-armdry-builds clean.Closes #<this issue>, and the PR description states plainly whether step 4 is expected to pass or whether anything was left non-declarative.