Skip to content

Install Prosody XMPP Server

Installation

Install the repository:

sudo -s
echo 'deb https://packages.prosody.im/debian focal main' | tee /etc/apt/sources.list.d/prosody.list
wget https://prosody.im/files/prosody-debian-packages.key -O- | apt-key add -

Update the apt cache and install Prosody:

apt update && apt install prosody lua-sec

Start and enable the Prosody service

systemctl start prosody
systemctl enable prosody

Open Firewall Ports

Add the following rules in the relevant sections of IPTables:

-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 5222 -j ACCEPT -m comment --comment "Prosody C2S"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 5269 -j ACCEPT -m comment --comment "Prosody S2S"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 5050 -j ACCEPT -m comment --comment "Prosody Proxy"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 5269 -j ACCEPT -m comment --comment "Prosody S2S"

DNS Records

Set DNS A records in your DNS management console for the various components we'll use:

example.com
conference.example.com
proxy.example.com
pubsub.example.com
upload.example.com
vjud.example.com

Set the following SRV records:

_xmpp-client._tcp               3600    IN  SRV    0 5 5222 example.com.
_xmpp-server._tcp               3600    IN  SRV    0 5 5269 example.com.
_xmpp-server._tcp.conference    3600    IN  SRV    0 5 5269 example.com.
_xmpp-server._tcp.proxy         3600    IN  SRV    0 5 5050 example.com.
_xmpp-server._tcp.pubsub        3600    IN  SRV    0 5 5269 example.com.
_xmpp-server._tcp.vjud          3600    IN  SRV    0 5 5269 example.com.

Set the following TXT record:

_xmppconnect                    3600    IN  TXT "_xmpp-client-xbosh=https://example.com:443/http-bind"

Create the Certificates

Issue an RSA certificate and install to a custom location

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com -d conference.example.com -d proxy.example.com -d pubsub.example.com -d upload.example.com -d vjud.example.com --keylength 4096 --key-file /etc/letsencrypt/rsa-certs/example.com/privkey.pem --ca-file /etc/letsencrypt/rsa-certs/example.com/chain.pem --cert-file /etc/letsencrypt/rsa-certs/example.com/cert.pem --fullchain-file /etc/letsencrypt/rsa-certs/example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/rsa-certs/example.com" --post-hook "find /etc/letsencrypt/rsa-certs/example.com/ -name '*.pem' -type f -exec chmod 640 {} \; -exec chown root:prosody {} \; -exec service nginx reload \; -exec service prosody reload \;" --renew-hook "find /etc/letsencrypt/rsa-certs/example.com/ -name '*.pem' -type f -exec chmod 640 {} \; -exec chown root:prosody {} \; -exec service nginx reload \; -exec service prosody reload \;" --dnssleep 60

and issue an ECC certificate

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com -d conference.example.com -d proxy.example.com -d pubsub.example.com -d upload.example.com -d vjud.example.com --keylength ec-384 --key-file /etc/letsencrypt/ecc-certs/example.com/privkey.pem --ca-file /etc/letsencrypt/ecc-certs/example.com/chain.pem --cert-file /etc/letsencrypt/ecc-certs/example.com/cert.pem --fullchain-file /etc/letsencrypt/ecc-certs/example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/ecc-certs/example.com" --post-hook "find /etc/letsencrypt/ecc-certs/example.com/ -name '*.pem' -type f -exec chmod 640 {} \; -exec chown root:prosody {} \; -exec service nginx reload \; -exec service prosody reload \;" --renew-hook "find /etc/letsencrypt/ecc-certs/example.com/ -name '*.pem' -type f -exec chmod 640 {} \; -exec chown root:prosody {} \; -exec service nginx reload \; -exec service prosody reload \;" --dnssleep 60

Installing Extra Modules

First, we'll install mercurial:

apt update && apt install mercurial

Change to the /opt directory and download the modules:

cd /opt
hg clone https://hg.prosody.im/prosody-modules/ prosody-modules

Change the default ownership:

chown -R root:prosody /opt/prosody-modules
chmod g+s /opt/prosody-modules
find /opt/prosody-modules -type d -exec chmod g+s {} \;

Whenever we need to update, witch to the prosody-modules directory and run:

hg pull --update

Configuring Nginx for BOSH

Assuming that your XMPP domain is example.com, edit the Nginx server block:

nano /etc/nginx/sites-available/example.com

and add the following location block:

    location /http-bind {
        proxy_pass  http://localhost:5280/http-bind;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_buffering off;
        tcp_nodelay on;
    }

Setting up http_upload_external

Now we'll install an HTTP service for dealing with file attachments. This will be used in conjunction with the http_upload_external Prosody mod which we'll configure later:

Installation and initial configuration

apt update && apt install git uwsgi uwsgi-plugin-python3 python3-flask
cd /opt
git clone https://github.com/horazont/xmpp-http-upload

and we'll create an extra uwsgi ini file:

nano /opt/xmpp-http-upload/xmpp-http-upload.ini

Enter the following:

[uwsgi]
socket = 0.0.0.0:9002
processes = 5
threads = 1
auto-procname = true
procname-prefix-spaced = [xmpp-http-upload]
uid = nginx
gid = nginx
need-plugin = python3
chdir = /opt/xmpp-http-upload/
pythonpath = /opt/xmpp-http-upload/
wsgi-file = /opt/xmpp-http-upload/xhu.py
enable-threads = true
offload-threads = 10
env = XMPP_HTTP_UPLOAD_CONFIG=/opt/xmpp-http-upload/config.py

Now we'll create and edit the config file:

cp /opt/xmpp-http-upload/config.example.py /opt/xmpp-http-upload/config.py
nano /opt/xmpp-http-upload/config.py

Make sure the following are set and enter a secret key:

SECRET_KEY = b'your-secret-key'
DATA_ROOT = "/var/lib/xmpp-http-upload"
ENABLE_CORS = False
NON_ATTACHMENT_MIME_TYPES = [
    "image/*",
    "video/*",
    "audio/*",
    "text/plain",
]

Change ownership and permissions of the files:

chown -R nginx:prosody /opt/xmpp-http-upload
find /opt/xmpp-http-upload -type d -exec chmod g+s {} \;
find /opt/xmpp-http-upload -type d -exec chmod 750 {} \;
find /opt/xmpp-http-upload -type f -exec chmod 640 {} \;

Create an upload directory:

mkdir /var/lib/xmpp-http-upload
chown -R nginx:prosody /var/lib/xmpp-http-upload
chmod g+s /var/lib/xmpp-http-upload

Creating the service

Create a service file:

nano /etc/systemd/system/uwsgi-http-upload.service

Enter the following:

[Unit]
Description=uWSGI XMPP HTTP Upload
After=syslog.target

[Service]
ExecStart=/usr/bin/uwsgi --ini /opt/xmpp-http-upload/xmpp-http-upload.ini
# Requires systemd version 211 or newer
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Enable and start the service:

systemctl daemon-reload
systemctl enable uwsgi-http-upload.service
systemctl start uwsgi-http-upload.service

Configuring Nginx

We'll create a server block now for nginx:

nano /etc/nginx/sites-available/upload.example.com

and enter the following:

server {

    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name upload.example.com;

    ssl_certificate /etc/letsencrypt/rsa-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/rsa-certs/example.com/privkey.pem;
    ssl_certificate /etc/letsencrypt/ecc-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/ecc-certs/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/ecc-certs/example.com/chain.pem;

    include /etc/nginx/custom-config/ssl.conf;
    include /etc/nginx/custom-config/header.conf;

    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    include /etc/nginx/bots.d/blockbots.conf;
    include /etc/nginx/bots.d/ddos.conf;

    if ($allowed_country = no) {
        return 403;
    }

    if ( $bad_querystring !~* "[OK]" ) {
        set $blockreason $bad_querystring;
        return 403;
    }

    if ( $bad_request !~* "[OK]" ) {
        set $blockreason $bad_request;
        return 403;
    }

    if ( $bad_request_method !~* "[OK]" ) {
        set $blockreason $bad_request_method;
        return 403;
    }

    location /upload/ {
        rewrite  ^/upload/(.*) /$1 break;
        include uwsgi_params;
        uwsgi_pass uwsgi://localhost:9002;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 50M;
        modsecurity_rules '
        SecRuleRemoveById 911100
        SecRuleRemoveById 920420
        ';
    }
}

Enable the site and reload Nginx:

ln -s /etc/nginx/sites-available/upload.example.com /etc/nginx/sites-enabled/
service nginx reload

Creating an apparmor profile

Now we'll create an apparmor profile for uwsgi:

nano /etc/apparmor.d/usr.bin.uwsgi-core
#include <tunables/global>

/usr/bin/uwsgi-core {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/python>

  capability setgid,
  capability setuid,

  /etc/mime.types r,
  /opt/xmpp-http-upload/ r,
  /opt/xmpp-http-upload/config.py r,
  /opt/xmpp-http-upload/xhu.py r,
  /proc/*/fd/ r,
  /proc/sys/net/core/somaxconn r,
  /run/systemd/notify w,
  /usr/bin/uname mrix,
  /usr/bin/uwsgi-core mr,
  owner /opt/xmpp-http-upload/xmpp-http-upload.ini r,
  owner /proc/sys/kernel/random/boot_id r,
  owner /var/lib/xmpp-http-upload/** rw,

}

Reload apparmor:

service apparmor reload

Check the profile is enforced with aa-status

Finally, we'll create a cron job to purge uploaded files older than 6 months:

crontab -e

Add the following line:

@daily   find /var/lib/xmpp-http-upload/ -mindepth 1 -type d -mtime +180 -print0 | xargs -0 -- rm -rf

The Prosody Config File

Config file:

-- Prosody XMPP Server Configuration

-- Server settings

admins = {"user@example.com" }
allow_registration = false
c2s_require_encryption = true
s2s_require_encryption = true
s2s_secure_auth = true
pidfile = "/run/prosody/prosody.pid"
daemonize = false
authentication = "internal_hashed"
storage = "internal"


-- SSL

https_ssl = {
        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem"; 
}

ssl = {
        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem"; 
}

checkcerts_notify = 7
certificates = "certs"
hsts_header = "max-age=31556952"


-- BOSH configuration

consider_bosh_secure = true;
cross_domain_bosh = true;


-- Plugin Paths

plugin_paths = { "/opt/prosody-modules" }


-- Modules Enabled

modules_enabled = {

        -- Generally required
        "roster"; -- Allow users to have a roster. Recommended ;)
        "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
        "tls"; -- Add support for secure TLS on c2s/s2s connections
        "dialback"; -- s2s dialback support
        "disco"; -- Service discovery

        -- Custom
        "carbons"; -- Keep multiple clients in sync
        "carbons_copies"; -- carbons for legacy clients
        "pep"; -- Enables users to publish their mood, activity, playing music and more
        "private"; -- Private XML storage (for room bookmarks, etc.)
        "blocklist"; -- Allow users to block communications with other users
        "vcard4"; -- Allow users to set vCards
        "default_bookmarks"; -- Default bookmarks for all users
        "privacy_lists";
        "bookmarks"; -- Bookmarks for xmpp channels
        "http_avatar"; -- serves avatars from local users
        "strict_https"; -- force https on web
        "offline"; -- Store offline messages
        "idlecompat"; -- This module adds XEP-0319 idle tags to presence stanzas
        "bidi"; -- This module implements XEP-0288: Bidirectional Server-to-Server Connections
        "addressing"; -- This module is a partial implementation of XEP-0033: Extended Stanza Addressing
        "webpresence"; -- Allows you to publish your Jabber status to your blog or website
        "watchuntrusted"; -- notify on unencrypted s2s
        "version"; -- Replies to server version requests
        "uptime"; -- Report how long server has been running
        "time"; -- Let others know the time here on this server
        "ping"; -- Replies to XMPP pings with pongs
        "mam"; -- Store messages in an archive and allow users to access it
        "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
        "bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
        "websocket"; -- XMPP over WebSockets
        "server_contact_info"; -- Publish contact information for this service
        "announce"; -- Send announcement to all online users
        "watchregistrations"; -- Alert admins of registrations
        "legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
        "smacks";
        "net_multiplex";
        "presence_cache"; -- stores a timestamp of the latest presence received from users contacts
        "idlecompat";
        "uptime_presence";
        "checkcerts"; -- Inform admins before certificates expire
        "csi";
        "csi_battery_saver"; -- Use less battery on mobile phones
        "cloud_notify";
        "server_contact_info";
        "vcard_legacy";
        "log_auth";
        "posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
}

modules_disabled = {
        "http_upload";
}


-- Disco settings

disco_items = {
        { "example.com", "Example.com XMPP Server" };
        { "conference.example.com", "Example.com Chatrooms" };
        { "proxy.example.com", "Example.com SOCKS5 service" };
        { "pubsub.example.com", "Example.com Publish/Subscribe service" };
        { "upload.example.com", "Example.com File Uploads" };
        { "vjud.example.com", "Example.com User Directory" };
}


-- Contact settings

contact_info = {
        abuse = { "mailto:admin@example.com", "xmpp:user@example.com" };
        admin = { "mailto:admin@example.com", "xmpp:user@example.com" };
        security = { "mailto:admin@example.com", "xmpp:user@example.com" };
        support = { "mailto:admin@example.com", "xmpp:user@example.com" };
        feedback = { "mailto:admin@example.com", "xmpp:user@example.com" };
}


-- Archive settings

archive_expires_after = "2m"; -- Remove archived messages after 2 months
max_archive_query_results = 30;
default_archive_policy = "roster"; -- archive only messages from users who are in your roster
mam_smart_enable = true


-- Log settings

log = {
        info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
        error = "/var/log/prosody/prosody.err";
}


-- Watchers and notifications

registration_watchers = { "user@example.com" } -- mod_watchregistrations will use this list of users instead of the admin list
registration_notification = "$username registered on $host"

untrusted_fail_watchers = { "user@example.com" }
untrusted_fail_notification = "Establishing a secure connection from $from_host to $to_host failed. Certificate hash: $sha1. $errors"


-- Default bookmarks for new users

default_bookmarks = {
        { jid = "support@conference.example.com", name = "Support Room" };
}


-- Cloud_notify settings

push_notification_with_body = false -- Whether or not to send the message body to remote pubsub node
push_notification_with_sender = true -- Whether or not to send the message sender to remote pubsub node
push_max_errors = 16 -- persistent push errors are tolerated before notifications for the identifier in question are disabled
push_max_devices = 5 -- number of allowed devices per user


-- Proxy settings

proxy65_ports = { 5050 }


-- Virtualhosts

VirtualHost "example.com"
        name = "Example.com XMPP Server"
        enabled = true
        ssl = {
                key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem"; 
        }

        Component "conference.example.com" "muc"
                name = "Example.com chatrooms";
                restrict_room_creation = "local";
                muc_room_default_members_only = true;
                muc_room_default_public_jids = true;
                muc_log_by_default = true;
                muc_log_true_rooms = false;
                muc_log_all_rooms = false;
                max_history_messages = 30;
                ssl = {
                        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem";
                }
                modules_enabled = {
                        "muc_mam"; -- message archive in muc
                        "muc_mam_hints";
                        "muc_limits";
                        "vcard_muc"; -- This module adds the ability to set vCard for MUC rooms.
                }

        Component "proxy.example.com" "proxy65"
                proxy65_acl = { "example.com" }
                proxy65_address = "proxy.example.com"
                name = "SOCKS5 Bytestreams Service"
                ssl = {
                        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem";
                }

        Component "pubsub.example.com" "pubsub"
                pubsub_max_items = 10000
                modules_enabled = { "pubsub_feeds", "pubsub_text_interface" }
                ssl = {
                        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem";
                }

        Component "vjud.example.com" "vjud"

                ssl = {
                        key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                        certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem";
                }

Component "upload.example.com" "http_upload_external"
        http_upload_external_base_url = "https://upload.example.com/upload/"
        http_upload_external_secret = "ExtraSpamFightingTips"
        http_upload_external_file_size_limit = 50000000 -- 50 MB
        ssl = {
                key = "/etc/letsencrypt/rsa-certs/example.com/privkey.pem";
                certificate = "/etc/letsencrypt/rsa-certs/example.com/fullchain.pem";
        }

Restart Prosody after config changes:

service prosody restart

Creating an Apparmor profile for Prosody:

Create the profile:

nano /etc/apparmor.d/usr.bin.prosody

Enter the following:

#include <tunables/global>

/usr/bin/prosody flags=(complain) {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/openssl>
  #include <abstractions/ssl_keys>

  capability dac_override,
  capability dac_read_search,

  /etc/prosody/** r,
  /opt/prosody-modules/** r,
  /usr/bin/lua5.2 mrix,
  /usr/bin/prosody r,
  /usr/lib/prosody/** m,
  /usr/share/lua/** r,
  /var/lib/prosody/** rw,
  /var/log/prosody/* rw,
  /var/lib/xmpp-http-upload/** r,
  @{run}/prosody/* rwk,
  owner @{run}/prosody/prosody.pid rwk,

}

Use aa-logprof to monitor and once happy, enforce with aa-enforce

Useful Links

https://github.com/ThomasLeister/prosody-filer https://community.jitsi.org/t/error-on-prosody-without-any-reasons-no-key-present-in-ssl-tls-configuration-for-https-port-5281/17124/24 https://prosody.im/doc/setting_up_bosh https://community.hetzner.com/tutorials/prosody-debian9#optional-advanced-features https://groups.google.com/forum/#!topic/prosody-users/lPjWXYbYfeI https://blog.wirelessmoves.com/2019/05/configuring-prosody-for-ios-and-chatsecure-push.html https://github.com/ThomasLeister/prosody-config/blob/master/prosody.cfg.lua