Setting up ejabberd with LDAP

Learn how to set up ejabberd securely, link it up to your LDAP trust and configure the necessary DNS records.

Setting up ejabberd with LDAP

I'm really not sure how many more witty intros I can do on these articles. Think I'll give it a pass today. Let's get into this!

I'm setting this up on FreeBSD, so let's install all the relevant packages. Let's install ejabberd:

root@sin:~ # pkg install ejabberd
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following 14 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
	ejabberd: 20.12
	erlang: 21.3.8.18_1,4
	erlang-man: 21.3_2
[...]
[14/14] Extracting ejabberd-20.12: 100%

We'll want to create some DNS records to tell people where to look for our XMPP service. Let's get those out of the way now so they have time to propagate.

I'll be adding these to point to sin.zm.is.

_jabber._tcp.zm.is      86400   IN      SRV     0 0 5269 sin.zm.is
_xmpp-client._tcp.zm.is 86400   IN      SRV     0 0 5222 sin.zm.is
_xmpp-server._tcp.zm.is 86400   IN      SRV     0 0 5269 sin.zm.is

Let's test one of these out:

omega# dig srv _xmpp-server._tcp.zm.is +short
0 0 5269 sin.zm.is.

Let's move onto configuring ejabberd.

After issuing certificates with acme.sh and copying them into my FreeBSD jail every day with CRON (out of scope of this article), let's update the config with the LDAP definition:

host_config:
  zm.is:      # replace with your domain
    auth_method: ldap
    ldap_encrypt: tls
    ldap_tls_cacertfile: "/etc/ssl/ldapcert.pem" # replace with your cert path
    ldap_servers:
      - xf # this is your LDAP server
    ldap_uids:
      - uid # which attribute in LDAP is the username
    ldap_rootdn: "cn=passdn,ou=admin,dc=xf" # what DN to bind as
    ldap_password: "hunter2"                # password for said DN
    ldap_base: "ou=users,dc=xf"             # where to search
    ldap_filter: "(memberOf=cn=xmpp,ou=groups,dc=xf)" # check group membership (optional)
/usr/local/etc/ejabberd/ejabberd.yml

We're going to harden TLS a bit with the following lines in each listen stanza (don't copy this into any configs, see below for full config):

protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"

The protocol options are industry standard, and the cipher selected is either CHACHA20-POLY1305 or AES256-SHA384 - arguably the best options out there.

Here's the full file:

###
###              ejabberd configuration file
###
### The parameters used in this configuration file are explained at
###
###       https://docs.ejabberd.im/admin/configuration
###
### The configuration file is written in YAML.
### *******************************************************
### *******           !!! WARNING !!!               *******
### *******     YAML IS INDENTATION SENSITIVE       *******
### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY *******
### *******************************************************
### Refer to http://en.wikipedia.org/wiki/YAML for the brief description.
###

hosts:
  - zm.is

host_config:
  zm.is:
    auth_method: ldap
    ldap_encrypt: tls
    ldap_tls_cacertfile: "/etc/ssl/ldapcert.pem"
    ldap_servers:
      - zm
    ldap_uids:
      - uid
    ldap_rootdn: "cn=passdn,ou=admin,dc=xf"
    ldap_password: "hunter2"
    ldap_base: "ou=users,dc=xf"
    ldap_filter: "(memberOf=cn=xmpp,ou=groups,dc=xf)"

loglevel: 4
log_rotate_size: 10485760
log_rotate_date: ""
log_rotate_count: 1
log_rate_limit: 100

## If you already have certificates, list them here
certfiles:
  - /etc/ssl/private/fullchain
  - /etc/ssl/private/privkey

listen:
  -
    port: 5222
    ip: "10.1.0.141"
    module: ejabberd_c2s
    max_stanza_size: 262144
    shaper: c2s_shaper
    access: c2s
    starttls_required: true
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
  -
    port: 5222
    ip: "fd20:cead:faff::141"
    module: ejabberd_c2s
    max_stanza_size: 262144
    shaper: c2s_shaper
    access: c2s
    starttls_required: true
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
  -
    port: 5269
    ip: "10.1.0.141"
    module: ejabberd_s2s_in
    max_stanza_size: 524288
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
  -
    port: 5269
    ip: "fd20:cead:faff::141"
    module: ejabberd_s2s_in
    max_stanza_size: 524288
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
  -
    port: 5443
    ip: "10.1.0.141"
    module: ejabberd_http
    tls: true
    request_handlers:
      /admin: ejabberd_web_admin
      /api: mod_http_api
      /bosh: mod_bosh
#      /captcha: ejabberd_captcha
      /upload: mod_http_upload
      /ws: ejabberd_http_ws
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
  -
    port: 5443
    ip: "fd20:cead:faff::141"
    module: ejabberd_http
    tls: true
    request_handlers:
      /admin: ejabberd_web_admin
      /api: mod_http_api 
      /bosh: mod_bosh
#      /captcha: ejabberd_captcha               
      /upload: mod_http_upload
      /ws: ejabberd_http_ws
    protocol_options:
      - "no_sslv3"
      - "no_tlsv1"
      - "no_tlsv1_1"
      - "cipher_server_preference"
      - "no_compression"
    ciphers: "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384"
#  -
#    port: 5280
#    ip: "::"
#    module: ejabberd_http
#    request_handlers:
#      /admin: ejabberd_web_admin
#      /.well-known/acme-challenge: ejabberd_acme
  -
    port: 1883
    ip: "10.1.0.141"
    module: mod_mqtt
    backlog: 1000
  -
    port: 1883
    ip: "fd20:cead:faff::141"
    module: mod_mqtt
    backlog: 1000

s2s_use_starttls: optional

acme:
  auto: false

acl:
  local:
    user_regexp: ""
  loopback:
    ip:
      - 127.0.0.0/8
      - ::1/128
  admin:
    user:
      - "m@zm.is"

access_rules:
  local:
    allow: local
  c2s:
    deny: blocked
    allow: all
  announce:
    allow: admin
  configure:
    allow: admin
  muc_create:
    allow: local
  pubsub_createnode:
    allow: local
  trusted_network:
    allow: loopback

api_permissions:
  "console commands":
    from:
      - ejabberd_ctl
    who: all
    what: "*"
  "admin access":
    who:
      access:
        allow:
          acl: loopback
          acl: admin
      oauth:
        scope: "ejabberd:admin"
        access:
          allow:
            acl: loopback
            acl: admin
    what:
      - "*"
      - "!stop"
      - "!start"
  "public commands":
    who:
      ip: 127.0.0.1/8
    what:
      - status
      - connected_users_number

shaper:
  normal: 1000
  fast: 50000

shaper_rules:
  max_user_sessions: 10
  max_user_offline_messages:
    5000: admin
    100: all
  c2s_shaper:
    none: admin
    normal: all
  s2s_shaper: fast

modules:
  mod_adhoc: {}
  mod_admin_extra: {}
  mod_announce:
    access: announce
#  mod_avatar: {}
  mod_blocking: {}
  mod_bosh: {}
  mod_caps: {}
  mod_carboncopy: {}
  mod_client_state: {}
  mod_configure: {}
  mod_disco: {}
  mod_fail2ban: {}
  mod_http_api: {}
  mod_http_upload:
    put_url: https://@HOST@:5443/upload
  mod_last: {}
  mod_mam:
    ## Mnesia is limited to 2GB, better to use an SQL backend
    ## For small servers SQLite is a good fit and is very easy
    ## to configure. Uncomment this when you have SQL configured:
    ## db_type: sql
    assume_mam_usage: true
    default: always
  mod_mqtt: {}
  mod_muc:
    access:
      - allow
    access_admin:
      - allow: admin
    access_create: muc_create
    access_persistent: muc_create
#    access_mam:
#      - allow
#    default_room_options:
#      mam: true
  mod_muc_admin: {}
  mod_offline:
    access_max_user_messages: max_user_offline_messages
  mod_ping: {}
  mod_privacy: {}
  mod_private: {}
  mod_proxy65:
    access: local
    max_connections: 5
  mod_pubsub:
    access_createnode: pubsub_createnode
    plugins:
      - flat
      - pep
    force_node_config:
      ## Avoid buggy clients to make their bookmarks public
      storage:bookmarks:
        access_model: whitelist
  mod_push: {}
  mod_push_keepalive: {}
  mod_register:
    ## Only accept registration requests from the "trusted"
    ## network (see access_rules section above).
    ## Think twice before enabling registration from any
    ## address. See the Jabber SPAM Manifesto for details:
    ## https://github.com/ge0rg/jabber-spam-fighting-manifesto
    ip_access: trusted_network
  mod_roster:
    versioning: true
  mod_s2s_dialback: {}
  mod_shared_roster: {}
  mod_stream_mgmt:
    resend_on_timeout: if_offline
  mod_vcard: {}
  mod_vcard_xupdate: {}
  mod_version:
    show_os: false

### Local Variables:
### mode: yaml
### End:
### vim: set filetype=yaml tabstop=8
/usr/local/etc/ejabberd/ejabberd.yml

You'll want to change all the IP addresses and the admin JID - m@zm.is (message me!).

Now, let's open 5222, 5269, 5443 and 1883 in our firewall in order to let it talk out.

I've done it like this:

xmpp="10.1.0.141"
xmpp_v6="fd20:cead:faff::141"
ports_xmpp="{ 5222, 5269, 5443, 1883 }"

rdr pass on $ext_if inet proto tcp from !<sshguard> to $ext_if port $ports_xmpp -> $xmpp
rdr pass on $ext_if inet6 proto tcp from !<sshguard> to $ext_if_v6 port $ports_xmpp -> $xmpp_v6

rdr pass on $br_if inet proto tcp from $net_v to 10.1.0.4 port $ports_xmpp -> $xmpp
rdr pass on $br_if inet6 proto tcp from $net_v_v6 to fd20:cead:faff::4 port $ports_xmpp -> $xmpp_v6

I've also got the bonus (last 2 lines) of being able to reach my XMPP server from within my VPN, since I've got split-horizon DNS:

x@qi ~ ยป host sin.zm.is
sin.zm.is has address 10.1.0.4
sin.zm.is has IPv6 address fd20:cead:faff::4

If you don't have a fancy network setup like mine you can do a simple pass rule:

ports_xmpp="{ 5222, 5269, 5443, 1883 }"
pass in on $ext_if inet proto tcp from any to any port $ports_xmpp keep state

I'd recommend using Gajim to connect to it, for it's fantastic OMEMO support. Adding an account is rather simple, and thanks to the SRV accounts we set up earlier, set-up will be automatic, with no need to put in the custom server we've set up for our domain. If you are actually running your XMPP server on the same domain, they aren't technically needed but are required if you want to adhere to the standard. So put them in.