XMPP

We are using XMPP for real-time communication. This page guides you through setting up the ejabberd XMPP Server.

Jail creation

As with most other services. Let’s start by creating a jail for ejabberd, install the ejabberd package and enabling the service.

bastille create ejabberd 14.2-RELEASE 10.0.0.30/8 bastille0
bastille pkg ejabberd install ejabberd
bastille sysrc ejabberd ejabberd_enable=YES

Make a copy of the example config files

cp /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberd.yml.example /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberd.yml
cp /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberdctl.cfg.example /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberdctl.cfg

Proxy & Certificates

Similar to our mailserver, ejabberd also requires access to the TLS certificates stored by caddy. The process will be similar. First, create a DNS A record for xmpp.<yourdomain.tld> pointing to the host. Then edit the caddy config:

micro /usr/local/bastille/jails/caddy/root/usr/local/etc/caddy/Caddyfile

Add the following entries to the Caddyfile

/usr/local/etc/caddy/Caddyfile
xmpp.ezdk.org {
	reverse_proxy 10.0.0.30:5280
}

Reload the caddy config:

bastille service caddy caddy reload

And from your local workstation send a request to the subdomain:

curl xmpp.ezdk.org

Now we can mount the directory containing the certificate and key for xmpp.<yourdomain.tld> into the ejabberd jail.

bastille mount ejabberd /usr/local/certificates/acme-v02.api.letsencrypt.org-directory/xmpp.ezdk.org /usr/local/certs nullfs ro 0 0

ejabberd configuration

First, we need to change the user under which ejabberd runs. For this, we need to edit the service file

micro /usr/local/bastille/jails/ejabberd/root/usr/local/etc/rc.d/ejabberd

and change the variable EJABBERDUSER to root

/usr/local/etc/rc.d/ejabberd
EJABBERDUSER=root

Caution

It is specifically not recommended to run the ejabberd service as root. But we have to use the same user which runs caddy (to read the certificates). NOTE FROM/TO AUTHORS: At least change the user for caddy/stalwart/ejabberd/etc to www

Edit the ejabberd config file:

micro /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberd.yml

Here we want to do the following adjustments:

  • Set our xmpp.* subdomain to host.
  • Set the paths to the certificates
  • Set the ip for all listeners to the jails internal ip address
  • Put all ejabberd_http listeners on port 5280 and disable TLS (since caddy handles it for us)
  • Add the internal jail ip to acl.loopback.ip and to api_permissions.‘public commands’.who.ip
  • Add ACL Rules for our admin account (that we will create afterwards)

This should result in a config file like this:

/usr/local/etc/ejabberd/ejabberd.yml
###
###              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:
  - xmpp.ezdk.org

loglevel: info

## If you already have certificates, list them here
certfiles:
  - /usr/local/certs/xmpp.ezdk.org.crt
  - /usr/local/certs/xmpp.ezdk.org.key

listen:
  -
    port: 5222
    ip: "10.0.0.30"
    module: ejabberd_c2s
    max_stanza_size: 262144
    shaper: c2s_shaper
    access: c2s
    starttls_required: true
  -
    port: 5223
    ip: "10.0.0.30"
    module: ejabberd_c2s
    max_stanza_size: 262144
    shaper: c2s_shaper
    access: c2s
    tls: true
  -
    port: 5269
    ip: "10.0.0.30"
    module: ejabberd_s2s_in
    max_stanza_size: 524288
    shaper: s2s_shaper
  -
    port: 5280
    ip: "10.0.0.30"
    module: ejabberd_http
    tls: false
    request_handlers:
      /admin: ejabberd_web_admin
      /api: mod_http_api
      /bosh: mod_bosh
      /captcha: ejabberd_captcha
      /upload: mod_http_upload
      /ws: ejabberd_http_ws

  -
    port: 5478
    ip: "10.0.0.30"
    transport: udp
    module: ejabberd_stun
    use_turn: true
    ## The server's public IPv4 address:
    turn_ipv4_address: "152.53.126.184"
    ## The server's public IPv6 address:
    # turn_ipv6_address: "2001:db8::3"
  -
    port: 1883
    ip: "10.0.0.30"
    module: mod_mqtt
    backlog: 1000

s2s_use_starttls: optional

acl:
  local:
    user_regexp: ""
  loopback:
    ip:
      - 10.0.0.30/8
      - 127.0.0.0/8
      - ::1/128
  admin:
    user: admin@xmpp.ezdk.org

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: "*"
  "webadmin commands":
    from: ejabberd_web_admin
    who: admin
    what: "*"
  "adhoc commands":
    from: mod_adhoc_api
    who: admin
    what: "*"
  "http access":
    from: mod_http_api
    who:
      access:
        allow:
          - acl: loopback
          - acl: admin
      oauth:
        scope: "ejabberd:admin"
        access:
          allow:
            - acl: loopback
            - acl: admin
    what:
      - "*"
      - "!stop"
      - "!start"
  "public commands":
    who:
      ip:
      - 10.0.0.30/8
      - 127.0.0.1/8
    what:
      - status
      - connected_users_number

shaper:
  normal:
    rate: 3000
    burst_size: 20000
  fast: 100000

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_adhoc_api: {}
  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
    custom_headers:
      "Access-Control-Allow-Origin": "https://@HOST@"
      "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS"
      "Access-Control-Allow-Headers": "Content-Type"
  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_muc_occupantid: {}
  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_bidi: {}
  mod_s2s_dialback: {}
  mod_shared_roster: {}
  mod_stream_mgmt:
    resend_on_timeout: if_offline
  mod_stun_disco: {}
  mod_vcard: {}
  mod_vcard_xupdate: {}
  mod_version:
    show_os: false

### Local Variables:
### mode: yaml
### End:
### vim: set filetype=yaml tabstop=8
root@ez1:~ #

Next, we need to setup a admin account for ejabberd.

bastille cmd ejabberd ejabberdctl register admin ezdk.org <a_very_strong_password>

Now we can start the ejabberd service

bastille service ejabberd ejabberd start

Open xmpp.<yourdomain.tld>/admin in your browser and log in with admin@xmpp.<yourdomain.tld> and the password you’ve set in the previous step. If you gain access to the ejabberd WebAdmn site, all went fine. Proceed to the next steps.

Port fowarding

All connections to ejabberd not through https (which also means most xmpp traffic), will be handled by ejabberd directly and not through caddy. Which means, we need to forward all other ports on which we have defined ejabberd listeners.

bastille rdr ejabberd tcp 5222 5222
bastille rdr ejabberd tcp 5223 5223
bastille rdr ejabberd tcp 5269 5269
bastille rdr ejabberd udp 5478 5478
bastille rdr ejabberd tcp 1883 1883

LDAP Integration

We want any user which is part of a specific group to be able to authenticate on the xmpp server (e.g. use the IM service). First, create the group “xmpp_users” though the lldap administration interface. Then create a user ro_ejabberd. We will use this as the user ejabberd uses to query the directory. Add it to the group lldap_strict_readonly.

You can also set up a testuser. E.g. xmpptest and add it to the xmpp_users group.

Then, edit the ejabberd config micro /usr/local/bastille/jails/ejabberd/root/usr/local/etc/ejabberd/ejabberd.yml and add the following lines:

/usr/local/etc/ejabberd/ejabberd.yml
## Authentication method
auth_method: [internal, ldap]
## DNS name of our LDAP server
ldap_servers: [10.0.0.10]
## Bind to LDAP server as "cn=Manager,dc=example,dc=org" with password "secret"
ldap_rootdn: "uid=ro_ejabberd,ou=people,dc=example,dc=com"
ldap_password: <password_of_ro_ejabberd>
ldap_port: 3890
## Define the user's base
ldap_base: "ou=people,dc=example,dc=com"
## We want to authorize users from 'xmpp_users' group only
ldap_filter: "(&(objectclass=person)(memberOf=cn=xmpp_users,ou=groups,dc=example,dc=com))"

Note

We could reuse the ro_admin account we used for nextclour earlier. But since the password is part of the config in cleartext, we better have an individual user which we can easily delete/recreate should something leak.

Note

One could also disable the internal user auth completely and just use auth_method: [ldap] but in this case, another admin user needs to be defined in the config file. Once which is present in the lldap directory.

Restart the ejabberd service:

bastille service ejabberd ejabberd restart

Now you can use @xmpp.ezdk.org with the users password to log in from any xmpp/jabber client - as long as the user is part of the xmpp_users group.

Frontend

We also want to serve a webclient from the host, so people can use the service without looking for a client first. We choose xmpp-web for this. It is a simple collection of html, js and css. So we can serve it with caddy directly.

Let’s grab the release from https://github.com/nioc/xmpp-web/releases and extract it in the roots /usr/local/www dir. This will create a new directory /usr/local/www/xmpp-web/ containing the content of the application.

fetch https://github.com/nioc/xmpp-web/releases/download/0.10.6/xmpp-web-0.10.6.tar.gz
tar xf xmpp-web-0.10.6.tar.gz -C /usr/local/www/

Now mount this directory into the caddy jail

bastille mount caddy /usr/local/www/xmpp-web /usr/local/www/xmpp-web nullfs ro 0 0

Modify the local.js config file of xmpp-web:

micro /usr/local/xmpp-web/local.js

Change the domains and websocket transport to point to our xmpp server

/usr/local/www/xmpp-web/local.js
// eslint-disable-next-line no-unused-vars, no-var
var config = {
  name: 'Echtzeit Chat',
  transports: {
    websocket: 'wss://xmpp.ezdk.org/ws',
  },
  hasGuestAccess: false,
  hasRegisteredAccess: true,
  anonymousHost: null,
  // anonymousHost: 'anon.domain-xmpp.ltd',
  isTransportsUserAllowed: false,
  hasHttpAutoDiscovery: false,
  resource: 'EZ Web XMPP',
  defaultDomain: 'xmpp.ezdk.org',
  defaultMuc: 'conference.xmpp.ezdk.org',
  // defaultMuc: 'conference.domain-xmpp.ltd',
  isStylingDisabled: false,
  hasSendingEnterKey: false,
  connectTimeout: 5000,
  pinnedMucs: [],
  logoUrl: '',
  sso: {
    endpoint: false,
    jidHeader: 'jid',
    passwordHeader: 'password',
  },
  guestDescription: '',
}

Edit the Caddyfile and add the following block to the config:

/usr/local/etc/caddy/Caddyfile
chat.ezdk.org {
	root * /usr/local/www/xmpp-web
	file_server
}

And reload the config

bastille service caddy caddy reload