Self hosted XMPP server (on OpenBSD)

2019-04-25

Update 2020-12-15: I've adapted this ansible role to match my needs. You can take a look at it here. This is running on 6.8-current.

Tested on OpenBSD 6.5 (prosody version 0.11.2)

Intro

Self-hosting an instant messaging service is quite simple. This guide shows how to do it using OpenBSD as a base system and XMPP as the messaging protocol.

The end result is an End-to-End encrypted chat system for 1:1 or multi-user conversations.

The software used for the server is prosody, and it's all based on this guide.

For the clients, I've tried Gajim for the desktop (works for Windows, Linux and *BSD), Conversations on the phone (Android) and profanity on the terminal (works almost everywhere). There's a client for iOS called ChatSecure, but I have not tried it.

Rationale

I used to host my messaging services back in the day. People stopped using this for some reason, and then came all the Whatsapp and co. So all that was forgotten.

Although I never used whatsapp, on recent times I've been testing some instant messaging systems, but none of them were good enough. In the end, all rely on central systems, often owned by companies that have to make money from somwhere. Most of the times is you (one way or another) even if they say the service is free.

I wanted something simple, client independent, secure (well, as secure as possible ...), easy to use from the client point of view and easy to manage from the server part. My goal is to replace things like Signal that I use with my family and friends.

XMPP is federated, just like email is. And with recent extensions like easy to use End-to-End encryption and http file sharing it's a viable solution for resilient and secure instant messaging system, that does not spy on you (no more than encrypted email for instance).

For now is not a complete replacement, as it does not provide VoIP, but is a start. I may look for voice alternatives or dig deeper for a jabber client that supports voice.

Previous steps (DNS and TLS)

Some DNS configuration is needed for this guide. If you are not using file uploads or multi-user chat, then is probably fine if your root dns name points to the machine that will host the xmpp server. If not, you'll have to define some SRV records, and also any record you may use for the mentioned services. It may look like this (config depends on your DNS provider):

_xmpp-client._tcp 1800 IN SRV 5 0 5222 server.mydomain.com.
_xmpp-server._tcp 1800 IN SRV 5 0 5269 server.mydomain.com.

This will tell xmpp clients and other servers trying to reach your accounts where (host and port) to knock.

In this particular case I configured also multi-user chat and http file uploads, so I defined uploads, proxy and groups as CNAME of the server's A record.

I also configured acme-client(1) and httpd(8) to get certificates from letsencrypt, so all communications client/server and server/server is encrypted.

How to do that is out of the scope of this guide, just read the man pages, it's quite easy to do. The only detail to take into account is that is better to have all the domains/subdomains with its own cert and into separated folders containing the certificate and the private key. This important for certificate import on prosody later on. So I ended up configuring it to store certs on a structure like:

/etc/ssl/letsencrypt/
|-- mydomain.com
|   |-- cert.pem
|   |-- fullchain.pem
|   `-- privkey.pem
|-- groups.mydomain.com
|   |-- cert.pem
|   |-- fullchain.pem
|   `-- privkey.pem
...

Server install

Install the server is as easy as:

$ doas pkg_add prosody

Server config

So here comes the fun part.

First you should get the community modules. Some of them provide functionality that is needed on any modern IM system.

The way to do that is cloning the mercurial repository. I did not want to have it installed on my server, so I cloned it on my desktop machine and synced to the server. So, on my desktop I did:

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

Then I uploaded it to /usr/local/lib/prosody-modules/ on the server. Here's the important parts I changed from the config files and why:

Community modules location:

plugin_paths = { "/usr/local/lib/prosody-modules" }

List of globally enabled modules:

modules_enabled = {
                "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
                "carbons"; -- Keep multiple clients in sync
                "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
                "vcard"; -- Allow users to set vCards
                "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
                "register"; -- Allow users to register on this server using a client and change passwords
                "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
                "server_contact_info"; -- Publish contact information for this service
                "vcard4";
                "vcard_legacy";
                "smacks"; -- XEP-0198: Stream Management, keep chatting even when the network drops for a few seconds
                "csi_simple";
                "bookmarks";
                "cloud_notify"; -- XEP-0357: Push Notifications.
}

Disable registration, as this will not be a public server. This is the default, but just check it just in case.

allow_registration = false

Force clients to use encrypted connections

c2s_require_encryption = true

Force servers to use encrypted connections.

s2s_require_encryption = true

Force certificate authentication for server-to-server connections. This may bring problems with servers that use self-signed certificates. Today nobody should be using that, as there are alternatives like letsencrypt but, if you have some server you want to talk to that uses self-signed certs, check s2s_insecure_domains

s2s_secure_auth = true

Location of directory to find certificates in (relative to main config file), on OpenBSD that's /etc/prosody/certs

certificates = "certs"

Virtual host. You can have many, for many domains. In my case this is just one personal domain. I limited the uploads to 9MB, but you can set up any other limit. Keep in mind that there's a 10MB limit for http_max_content_size

VirtualHost "mydomain.com"
Component "uploads.mydomain.com" "http_upload"
http_upload_file_size_limit = 1024 * 1024 * 9 -- 9MB upload limit
Component "groups.mydomain.com" "muc"
modules_enabled = { "muc_mam", "vcard_muc" }
Component "proxy.mydomain.com" "proxy65"

At this point you can import the certificates you got from letsencrypt (or from any other CA), with the command:

prosodyctl --root cert import /etc/letsencrypt/letsencrypt

That will copy all needed files to /etc/prosody/certs so they are accessible to the prosody daemon. Now you can start the daemon:

doas rcctl start prosody

To make it permanent on boot, add it to the pkg_scripts on /etc/rc.conf.local.

Also remember to open ports on the firewall (pf or any other you may have in front of your server). They are:

5000 --> for proxying large file transfers between clients
5222 --> for client to server
5269 --> server to server
5281 --> default https port for http file transfers

Add accounts and client config.

In order to add an account to your new server just execute:

prosodyctl adduser user@mydomain.com

You'll be asked for the new password and that's it !

On the client side is usually enough to enter the jid (jabber id, in this example user@mydomain.com) and the password. As we configured the necessary dns records earlier, the client will discover to which server and port to contact to.

OMEMO

OMEMO Multi-End Message and Object Encryption is an extension to XMPP that provides encryption for 1:1 and multi-user chats.

This is key for a secure chat system. Some of the most popular xmpp clients already support it. Conversations, for instance, has it enabled by default. Do not forget to set it up on your client.

OMEMO trusts devices on first use, then you should check the key fingerprints to see if the person you're talking to is who it claims to be.

Is a good practice to publish those fingerprints on some place public, like you would do with your GnuPG public key. In fact you could sign those to provide some proof of ownership.

On Conversations, for instance, you can later verify a contact fingerprint via a QR code. From that moment no other key will be allowed for that contact if you do not explicitly allow it. I think all clients should support that ...

Conclusion

Now you should have a working XMPP server. It can not only be used for IM, but also for notifications on your scripts using the libraries for your language of choice. Here you have some examples in perl, python and golang

Have any comments ? Send an email to the comments address.