Guide: Matrix on OpenBSD
Tags: Tech
In response to Discord's recent decision (decisions plural now, by the time this blog post is being posted...) to completely shoot itself in the foot, I've been looking into alternatives. For reasons I haven't had time to collect into a blog post like this, I have settled on Matrix and have started setting up a self-hosted "homeserver" for myself and trusted friends/family. I'll get around to posting that comparative analysis another time, but I figure this writeup is more immediately helpful.
I would recommend that anyone interested in setting up their own homeserver get in contact with the Matrix community directly for help and support. Starting out with a throwaway Matrix account on a well-known server like matrix.org using a web client like Cinny is pretty typical.
Choice of Homeserver
The first step was to pick a homeserver software. I won't write out a whole comparative analysis here, but to summarize: frankly, there's not that much difference. I went with Continuwuity (not a typo) most because it has far lower resource requirements than the more enterprise-focused Conduit.
There are plenty of other ones that make the same claim, to be clear. Conduit, Tuwunel, Dendrite, etc. It can be pretty hard to even tell the difference between them, externally. What swung it for me was that I found a BSD-focused room in Continuwuity's Matrix space, and was able to get help there. That room can be found at #bsd:continuwuity.org which is part of the space #space:continuwuity.org
In addition to the aforementioned BSD-focused Matrix channel as a whole, I also have to thank the user Lily from the Continuwuity forms, who in addition to their help in the Matrix chat also provided a forum post that was a huge help.
So this blog post will be a guide for Continuwuity just because that's what I wound up settling on.
Building for OpenBSD
One thing to note is that Continuwuity is written in Rust, which doesn't support BSD as well as it does a lot of other platforms. It wound up being enough for this, though, with only a minimum of hassle.
Importantly, you're going to need a fair bit of RAM on whatever server you're building Continuwuity on. Continuwuity v0.5.5 seems to be plenty content with 8 GB of RAM during the build process, but given the server itself works just fine with 4GB or maybe even 2GB of RAM you might want to build it on a different server and upload the binary. If you do then all the following steps should pretty much still apply as written, you'll just need to keep track of which system you're interacting with at any given time.
First of all, you're going to want to update your OpenBSD install to the newest version at time of writing, v7.8. This gets you Rust v1.90, rather than the v1.87 that you get with OpenBSD v7.7. I was able to patch Continuwuity v0.5.5 to compile with Rust v1.87, but it's not worth doing compared to just upgrading. Most of the problems were let chains, if you're curious; those are unstable until Rust v1.89 or v1.88.
Additionally, you're going to need to get LLVM configured. I wound up with LLVM 19, so that's the version you'll see below. The installation can be handled alongside git and rust, using pkg_add git rust llvm.
You'll also need to set up a couple of environment variables so that the LLVM install can be used, though. If you're also using LLVM 19 then this is what those look like:
LLVM_DIR=$(llvm-config-19 --libdir)
LIBCLANG_PATH=/usr/local/llvm19/lib/libclang.so.0.0
I recommend configuring your environment to persist these across boots, for convenience.
You should also at this point clone the Continuwuity repo to your system and check out the latest release. For instance if you're going to build Continuwuity v0.5.5:
cd ~
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
cd continuwuity
git checkout v0.5.5
From there the Rust build is straightforward and largely self-managing. You just need to tell it to not build one particular feature in the server with some command flags. That makes the build command:
cargo build --release --no-default-features --features=gzip_compression,zstd_compression,brotli_compression,media_thumbnail,release_max_log_level,element_hacks,url_preview,blurhashing,bindgen-runtime
You should now have the server binary at target/release/conduwuit and the base config file at conduwuit-example.toml. If you're building on a different system than you plan to run the server on, upload those two files now.
Configuring the server (Conduwuit.conf)
Before configuring the OpenBSD system to allow others to reach the server, we need to make some decisions about how the Matrix server will be configured. Copy your conduwuit-example.toml file to something like continuwuity.toml and open it. Here are my recommended changes (quotes, and the lack of them, ARE important!):
| Configuration Key Name | Recommended Value | Notes |
|---|---|---|
| server_name | "matrix.YOUR SERVER DOMAIN HERE" | Replace YOUR SERVER DOMAIN HERE with the domain of your server. For instance, my instance here uses matrix.orbitaplex.net |
| address | "127.0.0.1" | Matrix only supports IPv4 |
| port | 1558 | Just pick a TCP port number you like, frankly. This will only be used internally. Make sure to remember it for later, though. |
| database_path | "/var/lib/conduwuit" | |
| new_user_displayname_suffix | "" | Just lets your users pick the whole of their display names |
| allow_registration | true | To enable users to be made at all from clients rather than needing the admin console every time. |
| registration_token | Whatever the output of a run of openssl rand -base64 32 is, enclosed in double quotes. |
Seriously, don't make this predictable. If you want people to make user accounts on your server, just give them this string and then change it after they've used it. |
| require_auth_for_profile_requests | true | Privacy |
| allow_public_room_directory_over_federation | false | Privacy |
| allow_local_read_receipts | false | Privacy |
| allow_outgoing_read_receipts | false | Privacy |
| allow_legacy_media | false | Unless you want your users to be able to use your server as an image host outside Matrix, in which case leave this as true |
| url_preview_max_spider_size | 25600000 | This is specifically to get Youtube previews working... who knows what those Google engineers are on, but Youtube crawler responses contain many, many kilobytes of Javascript junk. |
| sentry_send_panic | false | |
| sentry_send_error | false | |
| server | "matrix.YOUR SERVER DOMAIN HERE:443" | |
| support_email | noone@nowhere.com | Or an actual email, if you want to advertise one. |
Configuring Relayd
If you read through the Matrix and Continuwuity docs you'll find that there are a bunch of things you can do with your server as it stands. However, in order to be meaningfully useful as an actual Matrix server, you'll need to configure a few extra things: a reverse proxy, TLS/SSL certs, and a service to run the server automatically.
The reverse proxy is easy: OpenBSD has the fantastic relayd program that can be configured for this purpose.
In my case, I already had relayd configured to serve this blog. I won't be including a full tutorial here on setting that up, but the following steps will assume that you also want to serve some static HTML files from the same server. There are just a couple things you should omit here if you're only going to be running a Matrix server, which I'll call out. But that also might make your TLS cert process somewhat different, so be forwarned of that.
The relayd program is controlled from a config file: /etc/relayd.conf
This file can be intimidating, but there's really just two concepts you need to understand: relays and protocols. Relays take incoming requests and route them, while protocols define ways in which requests are to be handled by the relays.
Since Matrix uses port 443 just like HTTPS, we'll use a single relay even if we want both an HTML server and a Matrix server. To define that relay, the following lines should be added to the relay.conf file (replace 1558 with whatever number you chose for the port config option above):
relay tls_relay {
listen on $ip4 port 443 tls
protocol https
forward to <www> port 8080 check icmp
forward to <matrix> port 1558 check icmp
}
This instructs the relayd program to do the following things:
- Wait for incoming requests on port 443, handling TLS handshakes with clients as needed.
- Apply the rules defined in the
httpsprotocol, in order. - If this request is set to be routed to the
<www>service, send it there to be responded to. - If this request is set to be routed to the
<matrix>service, send it there to be responded to. - Otherwise, ignore it.
As you can see, if you don't plan to serve an HTML website to the world wide web, you can simply omit the line corresponding to #3.
This means we need to define four more things in our file: the <www> service (assuming you didn't omit that), the <matrix> service, the https protocol, and our server's IPv4 address.
The first two and last of those are easy. Simply include the following four lines at the top of the file, the last being important for decent logging:
ip4="Your server's IPv4 address goes here"
table <www> { 127.0.0.1 }
table <matrix> { 127.0.0.1 }
log connection
Lines two and three there are called "tables", but the deeper functionality they unlock isn't going to be explored here. So you can think of them as simply defining that both our <matrix> and (assuming you're not omitting it) <www> services are going to be running on the same system as this relayd instance.
Defining the protocol is a bit more involved. That looks as follows:
http protocol https {
match header log "Host"
match header log "User-Agent"
match header log "Referer"
match url log
match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match request header set "Connection" value "close"
tcp { sack, backlog 128 }
tls { keypair YOUR SERVER DOMAIN }
match request header "Host" value "YOUR SERVER DOMAIN" forward to <www>
match request header "Host" value "www.YOUR SERVER DOMAIN" forward to <www>
match request header "Host" value "matrix.YOUR SERVER DOMAIN" forward to <matrix>
match request header "Host" value "matrix.YOUR SERVER DOMAIN:443" forward to <matrix>
# Add security headers
match response header append "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
match response header append "Cache-Control" value "public, max-age=86400"
match response header append "Content-Security-Policy" value "default-src 'self'; script-src 'self'; object-src 'none';"
match response header append "X-Content-Type-Options" value "nosniff"
match response header append "X-Frame-Options" value "SAMEORIGIN"
match response header append "Referrer-Policy" value "no-referrer"
match response header append "Permissions-Policy" value "interest-cohort=()"
match request header set "Accept-Encoding" value "gzip, deflate"
}
There's a lot more there than there was for the relay, mostly because a protocol does a lot more individual tasks than a relay. I won't explain each in detail, but this essentially amounts to handling the various security headers and routing decisions for the relay.
All you should need to do is replace YOUR SERVER DOMAIN with the domain of your server, including the top-level domain.
Amazingly, this is all pretty much in common between the matrix-alone and matrix-plus-HTML configurations we've been discussing. If you want the former, just omit the two lines referencing the <www> table.
Don't forget the line specifying :443 for Matrix-seeking hosts... some servers add that despite really not needing to, so you have to make sure to handle it.
TLS Certificates
The relayd configuration above will enable TLS/SSL for the server, but it does require one additional thing: an SSL certificate.
I recommend certbot for this, from the EFF. I won't provide a walkthrough of how to use it, since there is a lot of variability in how much information you want to give on your certs, etc. The EFF has a fantastic walkthrough that is even OpenBSD-specific on their website. Though I do recommend using something lighter than Python for the cert autorenewal. That's rather overkill.
Just make sure you have both your matrix.YOUR SERVER DOMAIN and YOUR SERVER DOMAIN domains on the cert. That's easy to miss.
One important caveat is that you MUST use RSA-based certs, not ECDSA! This appears to be a limitation in the configuration I've detailed here, though I'm not entirely sure where it arises from. It might be something to do with relayd but I've not been able to figure out exactly what. It should actually support those keys, but I wasn't able to get the configuration to work without RSA. If I figure out what the problem is I'll make a blog post here explaining it, I think, at which time I'll link this part here to that blog post.
Configuring a Daemon/Service
With all of the above complete, one last configuration step is required: creating a service. Or a daemon, to be pedantic.
This process is frankly arcane on OpenBSD. Explaining the why for each of these steps would require further explanations to even make sense, and so on... the overall effect is to remind one of little more than someone attempting to explain the lore for some long-running series like Kingdom Hearts or Homestuck.
So for this I shall simply provide a list of steps, to follow in order:
- Create a service file in your
rc.ddirectory:doas vim /etc/rc.d/conduwuit - Add to it the following lines exactly:
#!/bin/ksh
daemon_execdir="/home/continuwuity/src"
daemon_logfile="/var/log/continuwuity"
daemon="/home/continuwuity/conduwuit"
daemon_flags="-c /home/continuwuity/continuwuity.toml"
daemon_user="_continuwuity"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
pexp="${daemon} ${daemon_flags}"
rc_start() {
rc_exec ". ~/.profile; ${daemon} ${daemon_flags} >> ${daemon_logfile} 2>&1"
}
rc_cmd $1
- Create the service user _continuwuity:
doas useradd -g =uid -c "Service user for the Continuwuity Matrix server." -L daemon -s /sbin/nologin -d /home/continuwuity -m _continuwuity - Update your server's
daemonuser class to allow it to open more files at at time, for RocksDB:doas vim /etc/login.confthen find thedaemon:section and update bothopenfiles-maxandopenfiles-curto something along the lines of131072. Or higher, if you prefer. - Update your system-level value of same. Add a line like
kern.maxfiles=16777216to the file/etc/sysctl.conf, creating it if necessary. - Move/copy your server binary built earlier to
/home/continuwuity/conduwuit - Move/copy the config file built earlier to
/home/continuwuity/continuwuity.toml - Create the log file:
doas touch /var/log/continuwutityfollowed bydoas chmod 777 /var/log/continuwuity - Enable the service:
doas rcctl enable continuwuity - Start the service:
doas rcctl start continuwuity
You should see continuwuity (ok) printed to the terminal. If not, something in the above failed and I recommend reaching out for help in that Matrix room I linked near the start.
Create admin account
One additional thing you'll need to do is create the first account on the server. This is, by default, the server's "admin account". To make sure nobody can swoop in and make this account before you can, doing so requires a special code that will have been printed into your new logfile. Get that code by printing the contents of the logfile: cat /var/log/continuwuity If it's not in there then that's a problem.
Once you have that code, you can point your client of choice (except Element clients, at time of writing... some sort of mismatch in authentication something or other) at your new server. Register your new account, and you should be able to log in!
Federating
If all of the above went well then congrats! You're ready to start federating. I recommend keeping an eye on the Continuwuity space (address: #space:continuwuity.org) for updates, but you do you.
Do be aware that if you decide to join a really massive space such as the main Matrix space (address: #space:matrix.org) then it can take a LONG time to complete federation between that space and your new homeserver. And the load can be surprisingly immense.
This is normal the first time two servers connect, so do not worry. If future users on your homeserver do the same then the process won't need to happen a second time. So it might be a good idea to force your server to federate with at least that Matrix space overnight at some point if you're planning to ever have other users on your homeserver. That's essentially the largest and most system-stressing space, so getting it out of the way can be useful. Take care that your server is up to the task, though!
Next Steps
In order to really have feature-parity with Discord, it's going to be important to set up voice chats and screensharing. The above guide doesn't cover that, and indeed I've yet to add those capabilities. Theoretically it should be easier than this part of the process given it's all in Go rather than Rust, though, and Go is pretty well-supported on OpenBSD. I'll write up an update here once I get that working.