XMPP channel binding: serve auth from ejabberd's SQL store with generated passwords #690

Open
opened 2026-10-30 06:23:01 +00:00 by raucao · 0 comments
Owner

Context

Conversations (Android) 2.21.0 now requires channel binding by default for new installs. Our ejabberd authenticates against LDAP (auth_method: ldap per vhost in site-cookbooks/kosmos-ejabberd/templates/vhost.yml.erb), and LDAP cannot provide channel binding.

Goal: make kosmos.org advertise SCRAM-SHA-256-PLUS so channel binding works, while keeping LDAP as the source of truth for account existence and attributes.

Why LDAP can't do channel binding

  • ejabberd_auth_ldap only does an LDAP bind: store_type/1 -> external, plain_password_required/1 -> true.

  • SCRAM -PLUS requires the SCRAM verifier (storedkey/serverkey/salt/iterations) in ejabberd's own store (mnesia/SQL).

  • ejabberd_auth:store_type/1 collapses to external if any module is LDAP, and ejabberd_c2s:sasl_mechanisms/2 derives the advertised mechanism list from that single host-level value:

    Type = ejabberd_auth:store_type(LServer),
    Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256),
    (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted,
    

    So auth_method: [sql, ldap] yields store_type = external ⇒ no SCRAM at all (PLAIN only) for everyone, including users who have a verifier in SQL. The mechanism list is per host, not per user. ⇒ kosmos.org must use auth_method: sql only.

  • Existing LDAP hashes cannot be reused: akkounts stores {SSHA512}base64(SHA512(password+salt)+salt), a one-way digest that cannot be turned into a SCRAM verifier.

Chosen approach

Each account gets its own generated XMPP password (analogous to the existing email password) which is pushed into ejabberd's SQL store. LDAP remains the source of truth via a marker attribute.

Decisions

  • Separate generated XMPP password (not the LDAP/account password).
  • Hard cutover to [sql] for kosmos.org.
  • Scope: kosmos.org only. 5apps.com stays on LDAP (no channel binding there).
  • Existence state tracked in a new LDAP attribute.
  • Non-human accounts (bots) provisioned via chef-managed push.
  • On service disable: push a random password (block login, keep roster/MAM); unregister only on account deletion.

Changes

akkounts (kosmos/akkounts)

  • LDAP: new schemas/ldap/xmpp_password.ldif (new OID under 1.3.6.1.4.1.61554…), add to lib/tasks/ldap.rake add/delete lists and the ACI targetattr in LdapService#add_organization, add to LdapService#fetch_users. New LdapManager::UpdateXmppPassword. Marker only — store a bcrypt hash/timestamp, never plaintext (the verifier can't be read back).
  • API: EjabberdManagerService#change_password(user, password) (POST change_password) + EjabberdManager::SetPassword; treat non-200/error results as failures.
  • UI: Settings → Chat and Services → Chat — "Generate XMPP password" with current-account-password confirmation (mirror _email.html.erb), one-time new_xmpp_password view via session, and updated Services → Chat copy pointing users at the new password.
  • Lifecycle: XmppSetPasswordJob with retries on enable; on disable_service(:ejabberd) push a random password; unregister on account deletion.
  • Safety flag: gate generate/push behind a setting (e.g. Setting.xmpp_password_enabled? from an env var) so it can't run — and can't clobber LDAP passwords — before ejabberd is on SQL.
  • Tests: EjabberdManager::SetPassword service spec (WebMock), settings feature spec, LDAP service spec.

chef (this repo)

  • site-cookbooks/kosmos-ejabberd/recipes/default.rb + templates/vhost.yml.erb: add a per-host auth_method; kosmos.org = sql, 5apps.com = ldap (only render ldap_* when LDAP is used).
  • site-cookbooks/kosmos-ejabberd/templates/ejabberd.yml.erb: add change_password, unregister, check_account to the akkounts api_permissions allowlist; set auth_scram_hash: sha256.
  • Bot push: data-bag-driven recipe on the ejabberd node running change_password for each service account (idempotent, re-run on config changes).
  • Update doc/ejabberd.md and the InSpec test.

Hard-cutover runbook

  1. Pre-stage akkounts with the feature dark (flag off) so nothing pushes yet.
  2. Merge chef: kosmos.org → auth_method: sql, add allowlist entries, auth_scram_hash: sha256, and the bot push. (Human XMPP logins on kosmos.org will fail until users generate a password.)
  3. Enable the akkounts flag. Users generate their XMPP password (creates the row) and update their clients. Chef provisions bots.
  4. Audit: compare LDAP serviceEnabled=ejabberd against registered_users on kosmos.org.
  5. Rollback: revert to auth_method: ldap. Safe only if step 1's flag kept change_password away from LDAP — otherwise generated passwords would have overwritten real LDAP passwords.

Risks / notes

  • Clobber hazard: while LDAP is still configured, change_password writes to LDAP (eldap_pool:modify_passwd). The dark-launch flag in step 1 is what makes the hard cutover safe. (set_password tries auth methods in configured order and stops at the first success, so if methods are ever combined, SQL must come first — with LDAP first it would overwrite LDAP passwords.)
  • Everyone must act before they can chat, including desktop clients (Dino/Gajim/etc.), not just Conversations.
  • Set auth_scram_hash: sha256 before users generate passwords; changing it later invalidates them.
  • With [sql], the LDAP serviceEnabled gate no longer governs XMPP — akkounts becomes the provisioning authority (enable/disable/delete must map to push/random-password/unregister).
  • 5apps.com stays on LDAP and therefore still has no channel binding; revisit if its users rely on Conversations.
  • SQL users schema v2 (server_host, type) is auto-migrated via update_sql_schema: true.

Open item

Enumerate all non-human XMPP accounts so the chef push and the audit cover every one. Only hal8000 (HUBOT_XMPP_PASSWORD in the hubot data bag) is explicitly visible; check for botka, sockethub, bridge accounts, etc.

## Context Conversations (Android) 2.21.0 now **requires channel binding by default** for new installs. Our ejabberd authenticates against LDAP (`auth_method: ldap` per vhost in `site-cookbooks/kosmos-ejabberd/templates/vhost.yml.erb`), and LDAP cannot provide channel binding. **Goal:** make `kosmos.org` advertise `SCRAM-SHA-256-PLUS` so channel binding works, while keeping LDAP as the source of truth for account existence and attributes. ## Why LDAP can't do channel binding - `ejabberd_auth_ldap` only does an LDAP bind: `store_type/1 -> external`, `plain_password_required/1 -> true`. - SCRAM `-PLUS` requires the SCRAM verifier (storedkey/serverkey/salt/iterations) in ejabberd's own store (mnesia/SQL). - `ejabberd_auth:store_type/1` collapses to `external` if **any** module is LDAP, and `ejabberd_c2s:sasl_mechanisms/2` derives the advertised mechanism list from that single host-level value: ```erlang Type = ejabberd_auth:store_type(LServer), Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256), (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted, ``` So `auth_method: [sql, ldap]` yields `store_type = external` ⇒ **no SCRAM at all** (PLAIN only) for everyone, including users who have a verifier in SQL. The mechanism list is per host, not per user. ⇒ **`kosmos.org` must use `auth_method: sql` only.** - Existing LDAP hashes cannot be reused: akkounts stores `{SSHA512}base64(SHA512(password+salt)+salt)`, a one-way digest that cannot be turned into a SCRAM verifier. ## Chosen approach Each account gets its own **generated XMPP password** (analogous to the existing email password) which is pushed into ejabberd's SQL store. LDAP remains the source of truth via a marker attribute. ### Decisions - Separate generated XMPP password (not the LDAP/account password). - **Hard cutover** to `[sql]` for `kosmos.org`. - Scope: `kosmos.org` only. `5apps.com` stays on LDAP (no channel binding there). - Existence state tracked in a new LDAP attribute. - Non-human accounts (bots) provisioned via chef-managed push. - On service disable: push a random password (block login, keep roster/MAM); `unregister` only on account deletion. ## Changes ### akkounts (`kosmos/akkounts`) - **LDAP:** new `schemas/ldap/xmpp_password.ldif` (new OID under `1.3.6.1.4.1.61554…`), add to `lib/tasks/ldap.rake` add/delete lists and the ACI `targetattr` in `LdapService#add_organization`, add to `LdapService#fetch_users`. New `LdapManager::UpdateXmppPassword`. Marker only — store a bcrypt hash/timestamp, never plaintext (the verifier can't be read back). - **API:** `EjabberdManagerService#change_password(user, password)` (POST `change_password`) + `EjabberdManager::SetPassword`; treat non-200/error results as failures. - **UI:** Settings → Chat and Services → Chat — "Generate XMPP password" with current-account-password confirmation (mirror `_email.html.erb`), one-time `new_xmpp_password` view via session, and updated Services → Chat copy pointing users at the new password. - **Lifecycle:** `XmppSetPasswordJob` with retries on enable; on `disable_service(:ejabberd)` push a random password; `unregister` on account deletion. - **Safety flag:** gate generate/push behind a setting (e.g. `Setting.xmpp_password_enabled?` from an env var) so it can't run — and can't clobber LDAP passwords — before ejabberd is on SQL. - Tests: `EjabberdManager::SetPassword` service spec (WebMock), settings feature spec, LDAP service spec. ### chef (this repo) - `site-cookbooks/kosmos-ejabberd/recipes/default.rb` + `templates/vhost.yml.erb`: add a per-host `auth_method`; `kosmos.org` = `sql`, `5apps.com` = `ldap` (only render `ldap_*` when LDAP is used). - `site-cookbooks/kosmos-ejabberd/templates/ejabberd.yml.erb`: add `change_password`, `unregister`, `check_account` to the akkounts `api_permissions` allowlist; set `auth_scram_hash: sha256`. - Bot push: data-bag-driven recipe on the ejabberd node running `change_password` for each service account (idempotent, re-run on config changes). - Update `doc/ejabberd.md` and the InSpec test. ## Hard-cutover runbook 1. **Pre-stage akkounts** with the feature dark (flag off) so nothing pushes yet. 2. **Merge chef:** `kosmos.org` → `auth_method: sql`, add allowlist entries, `auth_scram_hash: sha256`, and the bot push. (Human XMPP logins on `kosmos.org` will fail until users generate a password.) 3. **Enable the akkounts flag.** Users generate their XMPP password (creates the row) and update their clients. Chef provisions bots. 4. **Audit:** compare LDAP `serviceEnabled=ejabberd` against `registered_users` on `kosmos.org`. 5. **Rollback:** revert to `auth_method: ldap`. Safe **only** if step 1's flag kept `change_password` away from LDAP — otherwise generated passwords would have overwritten real LDAP passwords. ## Risks / notes - **Clobber hazard:** while LDAP is still configured, `change_password` writes to LDAP (`eldap_pool:modify_passwd`). The dark-launch flag in step 1 is what makes the hard cutover safe. (`set_password` tries auth methods in configured order and stops at the first success, so if methods are ever combined, SQL must come first — with LDAP first it would overwrite LDAP passwords.) - **Everyone must act before they can chat**, including desktop clients (Dino/Gajim/etc.), not just Conversations. - Set `auth_scram_hash: sha256` **before** users generate passwords; changing it later invalidates them. - With `[sql]`, the LDAP `serviceEnabled` gate no longer governs XMPP — akkounts becomes the provisioning authority (enable/disable/delete must map to push/random-password/unregister). - `5apps.com` stays on LDAP and therefore still has no channel binding; revisit if its users rely on Conversations. - SQL `users` schema v2 (`server_host`, `type`) is auto-migrated via `update_sql_schema: true`. ## Open item Enumerate all non-human XMPP accounts so the chef push and the audit cover every one. Only `hal8000` (`HUBOT_XMPP_PASSWORD` in the `hubot` data bag) is explicitly visible; check for botka, sockethub, bridge accounts, etc.
raucao changed title from XMPP channel binding: serve kosmos.org auth from ejabberd's SQL store with generated passwords to XMPP channel binding: serve auth from ejabberd's SQL store with generated passwords 2026-10-30 06:24:27 +00:00
raucao added the
service
xmpp
featuresecurity
labels 2026-10-30 06:24:37 +00:00
raucao added this to the 2026 project 2026-10-30 06:30:53 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kosmos/chef#690