ejabberd + Biboumi + Docker = 🌞💬

Dominik 0a1cc30d31 added configuration based on .env vor 2 Jahren
EXTRAS 4824d078c2 moved configs from EXTRAS in final structure vor 2 Jahren
data 0a1cc30d31 added configuration based on .env vor 2 Jahren
.env.example 9042a99ca0 version 1.0 vor 2 Jahren
.gitignore 4824d078c2 moved configs from EXTRAS in final structure vor 2 Jahren
LICENSE bc9c1bc458 Initial commit vor 2 Jahren
README.md 0a1cc30d31 added configuration based on .env vor 2 Jahren
XMPP-Compliance-Tester.md 9042a99ca0 version 1.0 vor 2 Jahren
docker-compose.yml 5c84c18b15 removed www volume vor 2 Jahren
error-perm-fix.md 9042a99ca0 version 1.0 vor 2 Jahren

README.md

Dockerized ejabberd XMPP server with Biboumi IRC gateway

Introduction

This setup packs a ejabberd XMPP Server with a Biboumi IRC Gateway in a docker-compose.yml file.

The whole service lives in a single folder to ease backup and migration. All persistent data is stored in ./data/. All configurations are made in ejabberd.yml and in a .env file. For consistent backups, a cronjob one-liner, running in the ejabberd container, dumps daily the ejabberd DB into ./data/backup/.

It is assumed that on the docker host we're already running

  1. a jwilder/nginx-proxy reverse proxy
  2. together with jrcs/letsencrypt-nginx-proxy-companion to obtain letsencrypt TLS certificates,
  3. as well as watchtower to keep images up-to-date.

Design desitions:

  1. One host per instance / no vhosts
  2. No cluster setup
  3. Turn key, only need to set IP and hostname in .env
  4. Compliance with https://compliance.conversations.im/
  5. External acme client – for more flexibility when serving other containers on the same host. (Port 80 can be bound only once, so there can be only one acme client per host.)

Used docker images

  • This setup uses ejabberd/ecs from the ProcessOne ejabberd developers. The underlying Dockerfile builds the server from source and installs it into a Alpine image. The server runs as unprivileged user (uid 9000). This image doesn't come with many parameters or helper scripts, so all configurations are made in the ejabberd.yml configuration file. rroemhild/docker-ejabberd was not used as it didn't run reliably for me in July 2018. Still it is worth to take a look at, as it uses parameters and helper scripts to learn from.
  • Biboumi runs from louiz/biboumi. The image is provided by the Biboumi developer himself and only contains the binary as it is based on FROM scratch. All parameters are set using environment variables defined in docker-compose.yml. The server runs under a unprivileged user (uid 1000).

Open Ports

Marked [x] ports will be opened on the Docker host, so they should not be used by another service on the same host.

  • 5222:5222 Client 2 Server — bare minimum to speak with clients
  • 5223:5223 XMPP over TLS is actually deprecated, instead STARTTLS on port 5222 should be used. It is active for backward compatibility or could be moved to port 443 to ease usage behind firewalls.
  • 5269:5269 Server 2 Server — only needed when we want to speak with users from other servers
  • 5280:5280 Web admin (disabled), BOSH and WebSocket
  • 5443:5443 HTTP Upload
  • 113:8113 Identd so IRC servers can differentiate between users
  • 443!8080 Integrated webserver to serve static content, such as a javascript xmpp client. The port is exposed via the reverse proxy.
  • 5347:5347 Only used for internal unencrypted communication with the Biboumi (IRC gateway) component
  • ----:4560 XMLRPC — API, disabled

BOSH (previously known as 'HTTP binding' or "http-bind") is a technology to use XMPP over HTTP. This allows XMPP applications to run in web pages, but also any other HTTP-only environment such as behind restrictive firewalls.

Used volumes

ejabber runs under user ejabberd with uid/guid 9000. All data is stored in /home/ejabberd/ within the container.

  • ./data/conf/ejabberd.yml — main configuration file
  • .data/database/ — Erlang DB used to store all application data
  • .data/backup/ — place to to store DB dumps
  • .data/uploads/ — files transferred between users
  • .data/cron/backup.sh — job to daily dump the DB
  • ./opt/docker/proxy/data/certs/im.example.net/ — certificates and key material (to be changed as per the local docker host setup)
  • .data/www/ — static web content
  • ./data/biboumi/database/ — sqlite DB for Biboumi. The Biboumi configuration is defined via environment variables in docker-compose.yml.

DNS Setup

To allow service discovery, following DNS records have to be created.

im                              3600 IN A               1.2.3.4
*.im                            3600 IN CNAME           in
#_service._proto.name TTL class SRV priority weight port target
_xmpp-client._tcp.im.example.net.  3600 IN SRV    5 0 5222 im.example.net.
_xmpp-clients._tcp.im.example.net. 3600 IN SRV   10 0 5223 im.example.net.
_xmpp-server._tcp.im.example.net.  3600 IN SRV    5 0 5269 im.example.net.

To check the records: https://kingant.net/check_xmpp_dns/

Installation & Configuration

  1. Prepare data folders with proper permissions.

    inst_dir=/opt/docker/ejabberd
    mkdir -p $inst_dir/data/conf \
             $inst_dir/data/database \
             $inst_dir/data/backup \
             $inst_dir/data/upload \
             $inst_dir/data/cron \
             $inst_dir/data/www \
    && chown -R 9000:9000 $inst_dir/data/
        
    mkdir -p $inst_dir/data/biboumi/database
    chown -R 1000:1000 $inst_dir/data/biboumi/
    
  2. Obtain a vanilla ejabberd.yml configuration file. We'll briefly spawn a volatile container and copy the file to the host.

    docker run -d --rm --name ejabberd ejabberd/ecs
    docker cp ejabberd:/home/ejabberd/conf/ejabberd.yml $inst_dir/data/conf/
    docker kill ejabberd
    
  3. Configure the server / tune ejabberd.yml

    hostname=im.example.com
    config_file=$inst_dir/data/conf/ejabberd.yml
        
    # Set the hostname in .env to be used in docker-compose.yml
    echo "HOSTNAME=$hostname" > $inst_dir/.env
        
    # Set hostname and disable localhost configuration as we have no certificate for it
    sed -i \
      -e "s/^hosts:/hosts:\\n  - \"$hostname\"/" \
      -e '/^  - "localhost"/s/^/# /' \
      $config_file
    
    ## Disable web admin, api and oauth as not used and to reduce attack surface
    sed -i \
      -e '/web_admin: true/s/^/# /' \
      -e '/\/api/s/^/# /' \
      -e '/\/oauth/s/^/# /' \
      $config_file
          
    ## Enable TLS for BOSH and WebSockets
    sed -i \
      -e '/\/bosh/a\    tls: true' \
      $config_file
    
    ## Logs: Rotate at 1MB, don't keep old versions
    # The server is started in foreground and therefore logs are collected by docker,
    # so we actually don't need logs at all and can rotate rapidly without keeping old version
    sed -i \
      -e '/log_rotate_size/s/10485760/1048576/' \
      -e '/log_rotate_count/s/1/0/' \
      $config_file
    
    ## Point to letsencrypt certificates from jrcs/letsencrypt-nginx-proxy-companion,
    ## for details, see: https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion
    sed -i \
      -e '/^certfiles/a\  - \"/home/ejabberd/ssl/*.pem\"' \
      -e '/server.pem/s/^/# /' \
      $config_file
           
    ## Enforce TLS connection for c2s
    sed -i \
      -e '/    ## starttls_required: true/a\    starttls_required: true' \
      $config_file
        
    ## Enable XEP-0313 Message Archive Management,
    ## for details, see: https://xmpp.org/extensions/xep-0313.html)
    sed -i \
      -e '/  ## mod_mam/a\  mod_mam: {}' \
      $config_file
        
    ## Enable OMEMO (XEP-0384),
    ## for details, see: https://github.com/processone/ejabberd/issues/2425
    sed -i \
      -e '/OMEMO/,+3 s/^/#/' \
      $config_file
          
    ## Enable Roster Versioning (XEP-0237)
    sed -i \
      -e '/^  mod_roster: {}/a\  mod_roster:\n    versioning: true' \
      -e '/^  mod_roster: {}/s/^/#/' \
      $config_file
          
    ## Enable SOCKS5 Bytestreams Proxy (XEP-0065)
    sed -i -e '/mod_proxy65/s/## //' $config_file
    
    ## Enable legacy XMPP via TLS (XEP-0368)
    heredoc=`tempfile`
    cat > $heredoc <<'EOF'
      -
        port: 5223
        ip: "::"
        module: ejabberd_c2s
        tls: true
        max_stanza_size: 65536
        shaper: c2s_shaper
        access: c2s
    EOF
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
            
    ## Enable HTTP File Upload Listener(XEP-0363)
    heredoc=`tempfile`
    cat > $heredoc <<'EOF'
      -
        port: 5443
        ip: "::"
        module: ejabberd_http
        request_handlers:
          "": mod_http_upload
        tls: true
    EOF
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
        
    ## Enable HTTP File Upload Module
    ## with quota the keep files max 3 days and max size of 5MB per file
    heredoc=`tempfile`
    cat > $heredoc <<'EOF'
      mod_http_upload:
        put_url: "https://@HOST@:5443/upload"
        thumbnail: false # otherwise needs ejabberd to be compiled with libgd support
        max_size: 524288 # 5MB
      mod_http_upload_quota:
        max_days: 3
    EOF
    sed -i "/^modules:$/r $heredoc" $config_file
    rm $heredoc
        
    ## Add component for the IRC gateway
    heredoc=`tempfile`
    cat > $heredoc <<EOF
      -
        port: 5347
        ip: "::"
        module: ejabberd_service
        access: all
        hosts:
          "irc.$hostname":
            password: "secret"
    EOF
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
        
    ## Enable simple fileserver
    heredoc=`tempfile`
    cat > $heredoc <<'EOF'
      -
        port: 8080
        ip: "::"
        module: ejabberd_http
        request_handlers:
          "/": mod_http_fileserver
    EOF
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredocmkdir -p $inst_dir/data/biboumi/database
    chown -R 1000:1000 $inst_dir/data/biboumi/
    sed -i \
      -e '/## mod_http_fileserver:/s/## //' \
      -e '/mod_http_fileserver:/a\    default_content_type: "text/html"' \
      -e '/mod_http_fileserver:/a\    docroot: "/var/www/"' \
      -e '/mod_http_fileserver:/a\    directory_indices:\n      - "index.html"' \
      $config_file
          
    ## Disable legacy SSL protocols and ciphers
    sed -i \
      -e '/c2s_protocol_options:/ac2s_protocol_options:\n  - "no_sslv2"\n  - "no_sslv3"\n  - "no_tlsv1"\n  - "no_tlsv1_1"\n' \
      \
      -e '/s2s_protocol_options:/as2s_protocol_options:\n  - "no_sslv2"\n  - "no_sslv3"\n' \
      \
      -e '/## s2s_ciphers:/as2s_ciphers: "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"\n' \
      \
      -e '/## c2s_ciphers:/ac2s_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"\n' \
      \
      -e '/^    port: 5443/a\    protocol_options:\n      - "no_sslv2"\n      - "no_sslv3"\n      - "no_tlsv1"\n      - "no_tlsv1_1"\n    ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"\n' \
      \
      -e '/^    port: 5280/a\    protocol_options:\n      - "no_sslv2"\n      - "no_sslv3"\n      - "no_tlsv1"\n      - "no_tlsv1_1"\n    ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"\n' \
      $config_file
          
    
  4. Setup the cronjob for dumping daily the DB

    cat >$inst_dir/data/cron/dumb_db.sh  <<'EOF'
    #!/bin/sh
    /home/ejabberd/bin/ejabberdctl backup /home/ejabberd/backup/ejabberd.backup
    EOF
    chmod a+x $inst_dir/data/cron/dumb_db.sh
    
  5. Start the container. During the first startup the databases will be created.

    docker-compose up -d
    
  6. Create users

    ejabberd comes by default with no users. Privileges and ACLs for users are set in ejabberd.yml. In the default configuration file admin@localhost is set as privileged user already.

    To create new users, run from the docker host: docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl register <user> <domain> <password>

    We'll need to create at least a user for the IRC gateway.

    user=ircadmin
    pass=`apg -q -n1 -m12`
    docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl register $user $hostname $pass
        
    Further self-registration or LDAP authentication can be enabled in `ejabberd.yml`.
    
  7. (Optional) Run the XMPP compliance tester

    jar_file=ComplianceTester-0.2.3.jar
    dl_url=https://gultsch.de/files/$jar_file
    domain=$hostname
    docker run --rm -it --name=xmpptest openjdk:alpine \
    /bin/sh -c "wget $dl_url ; java -jar $jar_file $user@$domain $pass" \
    | tee $domain.txt
    
  8. (Optional) Setup a static website data/www/ can be used to serve a static site, e.g. for presenting compliance test results, a web xmpp client or client setup instructions

    cd $inst_dir/data/www
    git clone --depth=1 https://github.com/conversejs/converse.js.git
    mv converse.js webchat
    cp webchat/fullscreen.html webchat/index.html
    sed -i \
      -e '/analytics/d' \
      -e '/piwik/d' \
      -e "/bosh_service_url/s/conversejs.org/$hostname:5280/" \
      -e "/bosh_service_url/s/http-bind/bosh/" \
      webchat/index.html
    

Maintenance

Get a shell in the container

docker exec -it ejabberd sh

Control the server

docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl \

  • registered_users <host> List all registered users in HOST
  • unregister <user> <host> List all registered users in HOST
  • modules_available List the contributed modules available to install
  • modules_installed List the contributed modules already installed
  • mnesia Get details about the database
  • reload_config Reload ejabberd configuration file into memory (this will not start new servers)
  • connected_users list connected users with their resources
  • backup /home/ejabberd/backup/ejabberd.backup Backup database
  • install_fallback /home/ejabberd/backup/ejabberd.backup restores the db and makes it active after the next restart
  • help lists available commands
Biboumi

Biboumi documentation

Check if the IRC server certificate is valid

firefox https://www.sslshopper.com/ssl-checker.html#hostname=irc.example.net:6697

Whislist

  • Jingle? Jingle Video? WebRTC? STUN/TURN server to find ports for video calls, e.g. for Jingle ICE?
  • Jabber transport for Skype, e.g. spectrum
  • mirror vhost using mod_echo (this requires moving general configs in a vhost)

Notes and pitfalls

The Node and DB name changes with the hostname

Docker's randomly generated hostnames causes ejabberd to calculate different unique node and DB name. To prevent a new DB beeing created with each container restart, use docker run --hostname <hostname> or hostname: and domainname: in the docker-compose.yml.

/opt/docker/proxy/data/certs/<hostname> should be readable by ejabberd user.

Debugging

Inspect the vanilla container: docker run --rm -it --entrypoint /bin/sh ejabberd/ecs