# Moved from hosts/pinix/localServices/prosody/setup-prosody.nix on # 2026-09-06 (docs/intent/xmpp-umzug-pinix-blunix.md): same pinix-outage # resilience motivation as the earlier Forgejo move. Prosody is now # co-located with Traefik/ACME, LLDAP, and the agent-xmpp-listener that # already ran here, instead of reaching all three over the Tailnet. { config, pkgs, ... }: { services.prosody = { enable = true; # The included agenix files only exist at runtime. checkConfig = false; package = pkgs.prosody.override { withExtraLuaPackages = luaPackages: [ luaPackages.lualdap ]; withOnlyInstalledCommunityModules = [ "muc_moderation" ]; }; c2sRequireEncryption = true; s2sRequireEncryption = true; httpFileShare = { domain = "upload.${config.domain}"; http_external_url = "https://upload.${config.domain}/"; }; httpPorts = [ 5280 ]; httpsPorts = [ 5281 ]; extraModules = [ "http" "presence" ]; extraConfig = '' -- Advertises the address for SOCKS5 Bytestream (out-of-band file -- transfer) negotiation. This must be an address the two -- negotiating clients can actually reach -- now that c2s is bound -- directly on blunix's public interface, that is the public IPv4, -- not a Tailnet address a federated/remote client could never -- route to. proxy65_address = "${config.publicIPv4}" trusted_proxies = { "127.0.0.1", "::1", } prosodyctl_service_warnings = false Include "/run/agenix/${config.domain}.cfg.lua" Include "/run/agenix/motd.cfg.lua" ''; virtualHosts."${config.domain}" = { enabled = true; domain = config.domain; extraConfig = '' authentication = "ldap"; ldap_base = "ou=people,${config.ldapBaseDN}" ldap_server = "127.0.0.1:3890" ldap_rootdn = "uid=admin,ou=people,${config.ldapBaseDN}" filter = "(uid=$user)" -- Registered-account web client (hosts/blunix/localServices/xmpp-web.nix) modules_enabled = { "websocket"; } http_host = "chat.${config.domain}" http_external_url = "https://chat.${config.domain}/" http_paths = { websocket = "/xmpp-websocket"; } cross_domain_websocket = { "https://chat.${config.domain}"; } ''; ssl = { cert = "/var/lib/prosody/certs/${config.domain}.crt.pem"; key = "/var/lib/prosody/certs/${config.domain}.key.pem"; }; }; muc = [ { domain = "muc.${config.domain}"; name = "0ff.space public chatrooms"; restrictRoomCreation = "local"; moderation = true; roomDefaultLanguage = "de"; roomDefaultPublic = false; roomDefaultMembersOnly = true; extraConfig = '' muc_room_default_persistent = true muc_log_by_default = true muc_log_expires_after = "30d" muc_log_cleanup_interval = 4 * 60 * 60 ''; } ]; virtualHosts."bot.${config.domain}" = { enabled = true; domain = "bot.${config.domain}"; extraConfig = '' modules_enabled = { "admin_shell"; } authentication = "internal_hashed"; ''; ssl = { cert = "/var/lib/prosody/certs/${config.domain}.crt.pem"; key = "/var/lib/prosody/certs/${config.domain}.key.pem"; }; }; }; # Replaces the old manual SSH copy process # (hosts/pinix/localServices/prosody/README.md, "Refresh the TLS # certificate"), which existed only because Prosody and Traefik/ACME ran # on different hosts. Now that both run on blunix, this unit reads the # same certificate Traefik already obtained straight out of its ACME # store and installs it where Prosody expects it -- no key ever leaves # this host over the network. systemd.services.prosody-certs-sync = { description = "Sync Prosody's TLS certificate from Traefik's ACME store"; after = [ "traefik.service" ]; path = [ pkgs.jq pkgs.coreutils ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' set -eu dir=/var/lib/prosody/certs install -d -o prosody -g prosody -m 0750 "$dir" tmp_crt=$(mktemp) tmp_key=$(mktemp) trap 'rm -f "$tmp_crt" "$tmp_key"' EXIT jq -r '.cloudflare.Certificates[] | select(.domain.main == "${config.domain}") | .certificate' \ /var/lib/traefik/acme.json | base64 -d > "$tmp_crt" jq -r '.cloudflare.Certificates[] | select(.domain.main == "${config.domain}") | .key' \ /var/lib/traefik/acme.json | base64 -d > "$tmp_key" cert="$dir/${config.domain}.crt.pem" key="$dir/${config.domain}.key.pem" # Only touch the installed files -- and only reload Prosody -- when # the certificate actually changed (normally once per ACME renewal, # every ~60-90 days), not on every timer tick. if ! cmp -s "$tmp_crt" "$cert" 2>/dev/null; then install -o prosody -g prosody -m 0644 "$tmp_crt" "$cert" install -o prosody -g prosody -m 0640 "$tmp_key" "$key" # Prosody requires this unit, so on first activation (no cert # installed yet) prosody.service isn't up yet and its own start # job is still queued behind this one -- calling reload-or-restart # here would block waiting on a job that can't proceed until this # script returns, deadlocking forever. Only reload an # already-running Prosody; ordering alone brings up a fresh one # with the cert just installed above. if systemctl is-active --quiet prosody.service; then systemctl reload-or-restart prosody.service fi fi ''; }; systemd.timers.prosody-certs-sync = { description = "Periodic Prosody TLS certificate sync from Traefik"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "daily"; Persistent = true; }; }; systemd.services.prosody = { after = [ "prosody-certs-sync.service" ]; requires = [ "prosody-certs-sync.service" ]; }; }