modern selfhosted xmpp / jabber

modern selfhosted xmpp / jabber

I had my own jabber server running since a very long time. But, to be honest it was never my favorit. I had it just to have it ... the nerd problem.

Short before I turned it off, I had a reason to look again at it. If you want to communicate but not be bound to any company or service you must run your own communication server and connect it to the pipes of the federated structure.

Long story short, now my xmpp server is setup with all features you find in any commercial tool, now i'll write that down to spread the word and get more people in this free world of communication.

I'll not explain any plugin or modul - the documentation is out their, just read the specification if you want to get more knowledge.

XMPP Server

A few years in the past I had already switched to prosody, all other feels to bloated and it feels fast and easy. I run my prosody on a debian system and installed the prosody repository just to get the latest release.

Some more backround information can be found on this project page. It was useful in some ways during my journey to get this all up and running.


The configuration is based on my older 0.9 prosody configuration and just got all the additional fun.

-- Tip: You can check that the syntax of this file is correct
-- when you have finished by running: luac -p prosody.cfg.lua

-- this is based on the default 0.9 debian configuration file

---------- Server-wide settings ----------
admins = { "" }

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

	-- Not essential, but recommended
		"private"; -- Private XML storage (for room bookmarks, etc.)
		"vcard"; -- Allow users to set vCards

	-- These are commented by default as they have a performance impact
		--"privacy"; -- Support privacy lists
		--"compression"; -- Stream compression (Debian: requires lua-zlib module to work)

	-- Nice to have
		"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
		"pep"; -- Enables users to publish their mood, activity, playing music and more
		"register"; -- Allow users to register on this server using a client and change passwords

	-- Admin interfaces
		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
		--"admin_telnet"; -- Opens telnet console interface on localhost port 5582

	-- HTTP modules
		"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
		"http_files"; -- Serve static files from a directory over HTTP

	-- Other specific functionality
		"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
		--"groups"; -- Shared roster support
		"announce"; -- Send announcement to all online users
		--"welcome"; -- Welcome users who register accounts
		"watchregistrations"; -- Alert admins of registrations
		--"motd"; -- Send a message to users when they log in
		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
	-- Custom Plugins
		"carbons" ;

-- These modules are auto-loaded, but should you want
-- to disable them then uncomment them here:
modules_disabled = {
	-- "offline"; -- Store offline messages
	-- "c2s"; -- Handle client connections
	-- "s2s"; -- Handle server-to-server connections

-- enable http/bosh
http_paths = {
    files = "/";
    bosh = "/http-bind";

-- just listen on localhost with http/https bind
-- nginx will server that
http_dir_listing = true;
http_files_dir = "/srv/prosody/files";
http_ports = { 5280 }
http_interfaces = { "" }
https_ports = { 5281 }
https_interfaces = { "" }

-- Disable account creation by default, for security
-- For more information see
allow_registration = false;

-- Debian:
--   send the server to background.
daemonize = true;

-- Debian:
--   Please, don't change this option since /var/run/prosody/
--   is one of the few directories Prosody is allowed to write to
pidfile = "/var/run/prosody/";

-- no SNI is available, that is why I need the certificates 
-- in the global configuration too ...

ssl = {
    key = "/etc/prosody/certs/";
    certificate = "/etc/prosody/certs/";

-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.

c2s_require_encryption = true

-- Force certificate authentication for server-to-server connections?
-- This provides ideal security, but requires servers you communicate
-- with to support encryption AND present valid, trusted certificates.
-- NOTE: Your version of LuaSec must support certificate verification!
-- For more information see

s2s_secure_auth = false

-- Many servers don't support encryption or have invalid or self-signed
-- certificates. You can list domains here that will not be required to
-- authenticate using certificates. They will be authenticated using DNS.

--s2s_insecure_domains = { "" }

-- Even if you leave s2s_secure_auth disabled, you can still require valid
-- certificates for some domains by specifying a list here.

--s2s_secure_domains = { "" }

-- Select the authentication backend to use. The 'internal' providers
-- use Prosody's configured data storage to store the authentication data.
-- To allow Prosody to offer secure authentication mechanisms to clients, the
-- default provider stores passwords in plaintext. If you do not trust your
-- server please see
-- for information about using the hashed backend.

authentication = "internal_plain"

-- Select the storage backend to use. By default Prosody uses flat files
-- in its configured data directory, but it also supports more backends
-- through modules. An "sql" backend is included by default, but requires
-- additional dependencies. See for more info.

--storage = "sql" -- Default is "internal" (Debian: "sql" requires one of the

-- lua-dbi-sqlite3, lua-dbi-mysql or lua-dbi-postgresql packages to work)

-- For the "sql" backend, you can uncomment *one* of the below to configure:
--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename.
--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }

-- Logging configuration
-- For advanced logging see
-- Debian:
--  Logs info and higher to /var/log
--  Logs errors to syslog also
log = {
	-- Log files (change 'info' to 'debug' for debug logs):
	info = "/var/log/prosody/prosody.log";
	error = "/var/log/prosody/prosody.err";
	-- Syslog:
	{ levels = { "error" }; to = "syslog";  };

Include "conf.d/*.cfg.lua"

The virtual host configuration is by default splitted in a seperated file. Some of the settings can be done in the global section, but I did not cleaned that up yet.

VirtualHost ""

	-- Assign this host a certificate for TLS, otherwise it would use the one
	-- set in the global section (if any).
	ssl = {
		key = "/etc/prosody/certs/";
		certificate = "/etc/prosody/certs/";

admins = { "" }

-- Send messages to all connected clients
ignore_presence_priority = "true"

-- This can be done global?
-- File upload settings 
http_upload_path = "/srv/prosody/files";
http_upload_expire_after = 60 * 60 * 24 * 7 -- a week in seconds
default_archive_policy = true; -- other options are false or "roster";
max_archive_query_results = 500;

http_host = "";
http_external_url = ""

-- SSL will be done by NGINX
consider_bosh_secure =  true
cross_domain_bosh = true;

-- Websocket is extra
cross_domain_websocket = { "", "", "", "" };
consider_websocket_secure = true;

------ Components ------
-- Set up a MUC (multi-user chat) room server on
Component "" "muc"
	name = "jalogisch Chat Rooms"
	restrict_room_creation = "local";
	max_history_messages = 50; -- This is the largest number of messages that are allowed to be retrieved when joining a room
	max_archive_query_results = 50; -- This is the largest number of messages that are allowed to be retrieved in one MAM request
	muc_log_by_default = true;  -- Enable logging by default (can be disabled in room config)
	muc_log_all_rooms = true; -- set to true to force logging of all rooms

-- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
Component "" "proxy65"
	proxy65_acl = { "" }
	proxy65_address = ""

Component "" "pubsub"

DNS settings

The connection to the federation and most of the autoconfiguration for the clients need some DNS Settings. What is needed depends on the configured modules and components. But my configuration needed the following.

jabber		                   IN	A
			                   IN 	AAAA	2a01:4f8:c0c:2bbd::2
conference		               IN	CNAME
proxy			               IN	CNAME
pubsub			               IN	CNAME
_jabber._tcp		           IN	SRV	5	0	5269
_xmpp-client._tcp	           IN	SRV	5	0	5222
_xmpp-server._tcp	           IN	SRV	5	0	5269
_xmpp-server._tcp.conference   IN	SRV	5	0	5269
_xmpp-server._tcp.proxy	       IN	SRV	5	0	5269
_xmpp-server._tcp.pubsub	   IN	SRV	5	0	5269

HTTP proxy

As I have - if needed - nginx running as http server and proxy of choice, the following configuration serves all shared files and provide the general http interface.

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;

server  {
        listen 80;
        location /.well-known/acme-challenge/ {
            alias /var/www/le_root/.well-known/acme-challenge/;
        return 301$request_uri;

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off; # Requires nginx >= 1.5.9
  ssl_stapling on; # Requires nginx >= 1.3.7
  ssl_stapling_verify on; # Requires nginx => 1.3.7
  resolver valid=300s;
  resolver_timeout 5s;
  add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;

  ssl_certificate /etc/prosody/certs/;
  ssl_certificate_key /etc/prosody/certs/;

  ssl_trusted_certificate /etc/prosody/certs/;
  ssl_dhparam /srv/ssl/dhparams.pem;

  location /http-bind {
            proxy_pass  http://localhost:5280/http-bind;
            proxy_set_header Host $host;
            proxy_buffering off;
            tcp_nodelay on;

  location /xmpp-websocket {
            proxy_buffering off;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

  location / {
            proxy_set_header Host $host;
            proxy_buffering off;
            tcp_nodelay on;

SSL everything

Since Let's Encrypt went live their is no excuse not having your services with encryption enabled by default. I personal use if possible to issue, deploy and renew the certificates.

I use my default nginx host to verify all hosts that point to the IP with the following snippet in my default host configuration.

  location /.well-known/acme-challenge/ {
        alias /var/www/le_root/.well-known/acme-challenge/;

After all DNS settings are published, just issue the certificate: --issue -d -d -d -d --webroot /srv/www/le_root/

After that was done succesfull, install the certificates and establish the reload command after the renewal is done. --install-cert -d --cert-file /etc/prosody/certs/ --key-file /etc/prosody/certs/ --fullchain-file /etc/prosody/certs/ --capath /etc/prosody/certs/  --reloadcmd "service prosody restart && service nginx restart"

This way I never need to think of that and it will renewed short before the certificate is expired.

the reason

All this was done on a cold winter evening just because my little wants to chat with a friend of mine. The little just uses the old ipad mini to communicate with the family (yep all on iOS makes it easy) but the friend is on android.

It would have been easy to install some of the given messengers, but in the end we would act against the end user license of them - the toddlers age is far lower than the allowed age for all chat clients out in the wild.

So I just remembered my old jabber server and put a little work in this. Now we have a communication server that we are using with a growing group of people.

Clients are wild mixed, for Android most people use conversations on iOS is mixed between chatsecure and monal. On the desktop most Mac users tend to use adium and some uses spark which is also available for windows. The terminal lovers just use profanity.

We have now everything that most people know from commercial tools like WhatsApp, Telegram or what ever service you use - but we do not pay with our data for the service.

Find me in the pipes of the federated network at - happy jabber day.

Show Comments