Intro
In this post we'll see how to install and configure Prosody, an open-source XMPP
server. We will be deploying Prosody on a Hetzner cloud instance, provisioned
and configured with Guix and the powerful guix deploy
command.
XMPP and Prosody
XMPP is an open, extensible instant messaging protocol, originally introduced in 1999 and later formalised via a series of Internet Standard RFCs (RFC 3920, RFC 6120, RFC 6121, etc).
XMPP follows a client-server architecture, similarly to email. In order to use the protocol, you'll then need an account on an XMPP server and a client application on your devices, e.g. laptop and phone.
An XMPP account can be registered via one of the public providers (e.g. see this curated list). Alternatively, it's perfectly possible to self-host our own XMPP server, which is what we'll be doing here.
Guix Deploy and Hetzner
Guix is a Scheme-powered functional package manager and operating system that I might have mentioned once or twice in this blog already… ok, true, I talk about Guix all the time!
In this post we'll be leveraging a particular Guix command, guix deploy
, that
makes it possible to provision and install remote machines such as Virtual
Private Servers (VPSes). Today we'll be using the Hetzner backend,
hetzner-environment-type
, which has been added to Guix only recently (thanks
to Roman Scherer). The only other cloud integration available at the moment is
for DigitalOcean, but in the future it should be possible to extend guix
deploy
to work with virtually any provider.
What pushed me to try Hetzner is their competitive pricing. However, creating an account was pretty frustrating and I got very close to abandon my attempt half the way through. What was putting me off was the amount of personal information collected during the process, especially when I was asked a copy of an ID document. Eventually my account was automagically verified without me having to send any ID. I was able to access my account regularly at that point and things have been working pretty well since then.
System definitions
We will break the installation into three steps:
- provisioning of a base Guix system,
- DNS configuration,
- creation of TLS certificates and final setup of the XMPP server.
This can't be done in one go because we only get to know the instance IPv4 and IPv6 addresses after the initial provisioning.
We will need two Guix system definitions:
- a
system-base
for the provisioning step, - a
system-xmpp
that inherits fromsystem-base
and includes Certbot and Prosody.
Let's save the definitions in systems.scm
.
(define-module (systems) #:use-module (gnu) #:use-module (gnu packages bootloaders) #:use-module (gnu packages messaging) #:use-module (gnu services admin) #:use-module (gnu services certbot) #:use-module (gnu services messaging) #:use-module (gnu services networking) #:use-module (gnu services ssh) #:export (system-base system-xmpp)) (define %nftables-conf (plain-file "nftables.conf" "flush ruleset table inet firewall { chain inbound { type filter hook input priority 0; policy drop; iif lo accept icmpv6 type {nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert} accept ct state vmap {established: accept, related: accept, invalid: drop} tcp dport http accept tcp dport https accept tcp dport ssh accept tcp dport xmpp-client accept tcp dport xmpp-server accept } chain forward { type filter hook forward priority 0; policy drop; } }")) (define system-base (operating-system (host-name "xmpp") (timezone "Etc/UTC") (bootloader (bootloader-configuration (bootloader grub-bootloader) (targets (list "/dev/sda")) (terminal-outputs '(console)))) (initrd-modules (cons "virtio_scsi" %base-initrd-modules)) (file-systems (cons (file-system (mount-point "/") (device "/dev/sda1") (type "ext4")) %base-file-systems)) (users (list (user-account (name "alice") (group "users") (supplementary-groups '("wheel"))))) (sudoers-file (plain-file "sudoers" (string-append (plain-file-content %sudoers-specification) "%wheel ALL = NOPASSWD: ALL"))) (services (cons* (service dhcp-client-service-type) (service openssh-service-type (openssh-configuration (authorized-keys `(("alice" ,(plain-file "ssh.pub" "ssh-rsa AAAA...")))) (permit-root-login 'prohibit-password))) (service unattended-upgrade-service-type) %base-services)))) (define system-xmpp (operating-system (inherit system-base) (packages (cons* prosody (operating-system-packages system-base))) (services (cons* (service certbot-service-type (certbot-configuration (email "alice@example.com") (certificates (list (certificate-configuration (domains '("xmpp.example.com"))))))) (service nftables-service-type (nftables-configuration (ruleset %nftables-conf))) (service prosody-service-type (prosody-configuration (modules-enabled (cons* "groups" "mam" "carbons" %default-modules-enabled)) (virtualhosts (list (virtualhost-configuration (domain "xmpp.example.com")))))) (operating-system-user-services system-base)))))
Machine definitions
We will be using guix deploy
for the provisioning and configuration. We will
need two machine definitions, one for the base system and one for the final
installation. (In this context a machine definition is a Scheme record that
includes relevant information about a server.) Let's save the machine
definitions in machines.scm
.
(define-module (machines) #:use-module (gnu) #:use-module (gnu machine) #:use-module (gnu machine hetzner) #:use-module (systems) #:export (machine-base machine-xmpp)) (define machine-base (machine (operating-system system-base) (environment hetzner-environment-type) (configuration (hetzner-configuration (server-type "cx22") (ssh-key "/home/alice/.ssh/id_rsa"))))) (define machine-xmpp (machine (inherit machine-base) (operating-system system-xmpp)))
server-type
indicates the type of instance we want to use, e.g. in terms of
CPU, RAM, and storage. The available server types, and respective costs, are
listed here. For our use case, a relatively cheap and lightweight CX22
instance (2 VCPUs, 4GB RAM, 40GB SSD) should be fine. At the time of this
writing, a CX22
comes at about 4 GBP/month.
Provisioning
Create an API token on Hetzner and set it as the environment variable
GUIX_HETZNER_API_TOKEN
. We can provision a VPS and install the initial system
with:
guix deploy --load-path=. --expression='(list (@ (machines) machine-base))'
Under the hood, this involves quite a few steps and it may take a bit to
complete. If all goes well, the process returns the IPv4 and IPv6 addresses of
the provisioned machine. It should now be possible to access the machine via
SSH, as alice
and root
.
Now it's time to pick a domain name for the XMPP server, e.g.
xmpp.example.com
, and configure it to point to the provisioned machine.
XMPP
Finally, we can install Certbot, request TLS certificates to Let's Encrypt, and install Prosody, the XMPP server. This is all done with one simple command:
guix deploy --load-path=. --expression='(list (@ (machines) machine-xmpp))'
Before being used, Prosody needs some configuration. Let's SSH into the server and run the following commands. First, the TLS certificates need to be imported into Prosody.
prosodyctl --root cert import /etc/certs
Second, create one or more XMPP users with:
prosodyctl adduser alice@xmpp.example.com
It should now be possible to configure a XMPP client with the
alice@xmpp.example.com
account. Job done!
My XMPP handle is fnat@xmpp.fnat.me
, do reach out to me if you're an XMPP
user.
Outro
As it turns out, I'm in a self-hosting spree as this is what my last few posts
have been about. I'm glad that this time I've been able to use guix deploy
, it
makes the whole experience of provisioning and maintaining a cloud server so
much more pleasant to me.
In particular, requesting TLS certificates with the Certbot service has become
impressively easy - it's a blissful developer experience. The recently added
hetzner-environment-type
is also extremely pleasant to use. Big thanks to
Roman, the person who contributed this environment type, for the original work
on this and for helping me when I ran into a little problem.
Hope you've enjoyed this. I'll be Guixifing more and more of my cloud services in the future, so stay tuned!