An example configuration for getting started with Matrix on NixOS
A mini-article and RANT on how to deploy a trial Matrix homeserver for communications between family and friends without (mostly) depending on third-party services. “Mostly” implies that there are some dependencies:
owning a domain name
DNS management for this name to have an IPv4/v6 address reachable from the internet linked to your name
an internet connection
unfortunately, unlike XMPP, even if it's more convenient for various reasons, Matrix ties itself to other “trusted servers” and “vector.im”; it works without them too, but it's a bit crippled
For these reasons and plenty more besides, many people aren't fans of Matrix, but it has the advantage of working well enough once you've done a bit of swearing at the subpar documentation and various other issues. It's what you might call accessible for friends and family without IT skills, provided someone in the group sets up and maintains the instance for everyone else. Basically, you don't even need a third-party client; you can just provide Element web for those who want it. It has Android/iOS apps similar to the desktop version (along with various others that implement most of the features), so it's quite handy at the moment and might appeal to the many people jumping ship from Discord.
As a mobile client, I'd recommend Element Classic for now; for desktop, Element desktop. I'm not exactly thrilled with them, I think they're bloat monsters, but they get the job done and for starting out, I can't find anything better.
The initial minimum number of services is 3. Coturn to enable audio/video chat for those behind NAT/without IPv6, a Matrix server and a web server. Although not yet “production-ready”, after experimenting a bit with the Matrix servers available as NixOS modules, I chose Tuwunel, which is mature enough and doesn't seem to consume significant resources for a generic homeserver with family+friend only usage. Finally, a web server is needed to act as a proxy; in this case, I went with NGINX without a second thought. To get SSL without messing around with self-signed certificates or creating a CA for every client to avoid them, a dependency on Let's Encrypt/ACME is added.
I'll be updating this post soon with LiveKit and hopefully Element Call to support video conferencing for many participants (with the current setup you can do up to 3 or 4 max participants to a video-calls).
For now, I won't be covering the numerous “privacy” aspects of Matrix, such as how to manage room encryption, key backups, etc. There's a lot to it and it's a topic that takes time to explore, plus it doesn't make much sense for several reasons in a home test setup. So, let's get straight to the point.
Coturn
Some people complain about issues behind NAT; I haven't personally encountered them yet, so I'll just mention that if you try it and run into problems, you're not alone, but there's not much I can say firsthand as I haven't experienced them myself.
Thanks to the NixOS module, the configuration is fairly intuitive, even if you don't know anything about Coturn or the concept it implements beyond the vague idea of NAT traversal.
coturn = {
enable = true;
no-cli = true;
no-tcp-relay = false;
# these ports are default, exiplicit here for human reasons
tls-listening-port = 5349;
alt-tls-listening-port = 5350;
listening-port = 3478;
alt-listening-port = 3479;
# port range for clients communications, pick at your option
min-port = 43000;
max-port = 43200;
# to limit users ask for a pre-shared secret string
use-auth-secret = true;
static-auth-secret = "Pick a String Of Strange Text";
# your DNS must resolv this subdomain (choose any name
# you want, `turn' is just a common choice), ensure A and
# AAAA records, SRV records are optional for Matrix
realm = "turn.domain.tld";
# not a good Nix practice, but quick and simple
# ACME module save certs there
cert = "/var/lib/acme/turn.domain.tld/full.pem";
pkey = "/var/lib/acme/turn.domain.tld/key.pem";
extraConfig = ''
fingerprint
allowed-peer-ip=192.168.100.1 # server internal IP
external-ip=turn.domain.tld # server IP grabbed via DNS
# ban private IP ranges
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
''; # extraConfig
}; # coturn
Now we need the relevant fw setup for this service, a simple NFTable extras could be
# coturn listening-port
udp dport 3478 accept
tcp dport 3478 accept
# coturn tls-listening-port
udp dport 5349 accept
tcp dport 5349 accept
# coturn alt-listening-port
udp dport 3479 accept
tcp dport 3479 accept
# coturn alt-tls-listening-port
udp dport 5350 accept
tcp dport 5350 accept
# coturn client signaling port range
udp dport { 63000-63100 } accept
#tcp dport { 63000-63100 } accept
# matrix port, here just to put all in one place...
tcp dport 8448 accept
for both IPv4 and IPv6 (if you have and anyone should) of course. Finally the NGINX proxy pass for Coturn.
virtualHosts."turn.domain.tld" = {
enableACME = true;
forceSSL = true;
locations."/" = {
# pick the right port for you
proxyPass = "http://127.0.0.1:5349";
proxyWebsockets = true;
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_pass_header Authorization;
''; # extraConfig
}; # locations."/"
}; # virtualHosts."turn.domain.tld"
Tuwunel
Given that the configuration will be practically the same regardless of which Matrix server you choose, adapting it between them is fairly straightforward
matrix-tuwunel = {
enable = true;
user = "tuwunel";
group = "tuwunel";
stateDirectory = "tuwunel";
settings = {
global = {
# again choose as you like, just ensure the DNS is set up correctly,
# again A and AAAA records, SRV are optional.
server_name = "matrix.domain.tld";
database_backup_path = "/var/lib/tw-backup";
database_backups_to_keep = 1;
new_user_displayname_suffix = "🥚";
address = ["127.0.0.1" "::1"];
port = [8489];
allow_registration = true;
# to be given to aspiring new users, they will be asked for it when
# they register (make it a bit more safe than 2 words obviously)
registration_token = "Another Secret";
allow_encryption = true;
# choose here, allowing means been able to communicate with all
# federated matrix servers, the contrary means being tied to
# this very server
allow_federation = true;
# for users, admins are always allowed
allow_room_creation = true;
grant_admin_to_first_user = true;
federate_admin_room = false;
# other servers to trust for keys of federated users
# unfortunately mandatory to start...
trusted_servers = [ "matrix.org" ];
# the same you put in Coturn config, obviously
turn_secret = "Pick a String Of Strange Text";
turn_uris = [
"turns:turn.domain.tld:5349?transport=udp"
"turns:turn.domain.tld:5349?transport=tcp"
"turn:turn.domain.tld:3478?transport=udp"
"turn:turn.domain.tld:3478?transport=tcp"
]; # turn_uris
well_known = {
# the NGINX vhost listening for incoming client conn.
client = "https://matrix.domain.tld:8448";
server = "matrix.domain.tld";
};
}; # global
}; # settings
}; # matrix-tuwunel
And again the relevant NGINX vhost
# tuwunel
virtualHosts."matrix.domain.tld" = {
enableACME = true;
forceSSL = true;
listen = [
# matrix clients expect 8448 and 443
{ addr = "0.0.0.0"; port = 8448; ssl = true; }
{ addr = "0.0.0.0"; port = 443; ssl = true; }
];
extraConfig = ''
merge_slashes off;
'';
locations = {
"/" = {
proxyPass = "http://127.0.0.1:8489";
extraConfig = ''
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 150m;
''; # extraConfig
}; # "/"
}; # locations
}; # virtualHosts."matrix.domain.tld"
final touches
To allow Coturn and Tuwunel access to relevant SSL certs since ACME save them with it's system username and NGINX group (to allow NGINX reading them) the simplest thing is adding to system Coturn and Tuwunel groups the NGINX group. So:
users.users.turnserver.extraGroups = [ "nginx" ];
users.users.tuwunel.extraGroups = [ "nginx" ];
ACME config depend on your DNS provider, a generic example could be
acme = {
acceptTerms = true;
defaults = {
email = "admin@domain.tld";
# pick one from the list
dnsProvider = "ovh";
# the same
credentialsFile = "/run/secrets/ovh.api";
}; # defaults
certs."domain.tld" = {
domain = "domain.tld";
group = "nginx";
extraDomainNames = [
"turn.domain.tld"
"matrix.domain.tld"
]; # extraDomainNames
}; # certs."domain.tld"
}; # acme
Register side, typically in it's WebUI you have to create an API key, assign some permissions to it, like GET, POST and DELETE etc. I can't document this part since depend on your provider, while most operate in nearly identical manners.
Clients
Once done well... You are ready to create the first user via a client, maybe like Element Desktop, and announce to your family and friends the possibility to interact with a Matrix client on your new server.
Users could register themselves providing the shared Tuwunel secret you give to them, obviously choosing as homeserver matrix.domain.tld and not the default matrix.org or something else.
If you federate they can join rooms and spaces (collection of rooms) in other servers, at the price for you of hosting other servers contents, the one of the chosen rooms/spaces, who could bit little or big and could be legal or illegal.
Encrypted rooms cant' be read by the server admin, so well... Choose depending on the kind of server you want to host.