Prosody

Revision as of 23:07, 19 September 2025 by Tie-ling (talk | contribs) (Connect filer to Prosody: grant permission for prosody-filer)

Prosody is an open-source, modern XMPP server.

It is documented in the NixOS manual.

This page describes how to setup a walled-off Prosody instance for your organisation or family, with STUN/TURN support and http upload. This setup has server-to-server communication disabled.


SSL Certificate with ACME

This article assumes a working ACME configuration for certificate renewal.

{
  config,
  ...
}:
let
  sslCertDir = config.security.acme.certs."example.org".directory;
  domainName = "example.org";
in
{
  # further prosody & coturn configuration goes here
  ...
}

Set up Prosody

Open firewall for client to server c2s

  networking.firewall = {
    allowedTCPPorts = [
      # prosody client to server
      5222
      5223
    ];
  };

Authentication and XMPP modules

  services.prosody = {
    # this is a minimal server config with turn and http upload
    # all server to server connection are blocked
    enable = true;
    admins = [ "admin@${domainName}" ];
    allowRegistration = false;
    authentication = "internal_plain";
    s2sSecureAuth = true;
    c2sRequireEncryption = true;
    modules = {
      admin_adhoc = false;
      cloud_notify = false;
      pep = false;
      blocklist = false;
      bookmarks = false;
      dialback = false;
      ping = false;
      private = false;
      register = false;
      vcard_legacy = false;
    };
    xmppComplianceSuite = false;
  };

Setup Multi User Conference module

  services.prosody = {
    muc = [
      {
        domain = "muc.xmpp.${domainName}";
        # only admin can create public channels
        # everyone can create private chat rooms
        restrictRoomCreation = true;
      }
    ];
}

Setup virtual host and ssl

  services.prosody = {
    ssl = {
      cert = "${sslCertDir}/fullchain.pem";
      key = "${sslCertDir}/key.pem";
    };

    virtualHosts = {
      # xmpp server for "@example.org" is hosted on "xmpp.example.org"
      # use SRV records.
      "xmpp.${domainName}" = {
        domain = "${domainName}";
        enabled = true;
      };
    };
  };

You also need to set proper file permissions on the cert directory and key files. See ACME#Integration_with_service_modules.

Optional: use sqlite

  services.prosody = {
    extraConfig = ''
      storage = "sql"
      sql = {
        driver = "SQLite3";
        database = "prosody.sqlite"; -- The database name to use. For SQLite3 this the database filename (relative to the data storage directory).
      }
    '';
  };

sqlite seems to be more performant than the default file-backed storage driver.

Set up coturn

Open firewall ports

  networking.firewall = {
    allowedTCPPorts = [
      # prosody turn/stun with coturn
      # see /run/coturn/turnserver.cfg
      3478
      3479
      5349
      5350
    ];
    allowedUDPPorts = [
      # prosody turn/stun with coturn
      3478
      3479
      5349
      5350
    ];
    allowedUDPPortRanges = [
      # coturn
      {
        from = 49152;
        to = 65535;
      }
    ];
  };

Set up coturn

  age.secrets.prosody-coturn = {
    file = ./prosody-coturn.age;
    # -rw-r--r--
    mode = "644";
  };

  services.coturn = {
    # https://prosody.im/doc/coturn
    enable = true;
    listening-port = 3478;
    pkey = "${sslCertDir}/key.pem";
    cert = "${sslCertDir}/fullchain.pem";
    realm = "turn.xmpp.${domainName}";
    static-auth-secret-file = config.age.secrets.prosody-coturn.path;
    use-auth-secret = true;
  };

Connect to prosody

  services.prosody.virtualHosts."xmpp.${domainName}".extraConfig = ''
    turn_external_host = "turn.xmpp.${domainName}"
    turn_external_secret = "unfortunately this is a inline password"
  '';
  services.prosody.extraModules = [ "turn_external" ];

http upload with prosody-filer and nginx

Although newer prosody versions does not require external http upload to run, prosody-filer is still more performant than the built-in server. [1]

Open firewall ports

  networking.firewall = {
    allowedTCPPorts = [
      # prosody https upload
      443
    ];
  };

Setup prosody-filer service

  # set up directory for storing uploaded file
  systemd.services.prosody-filer.serviceConfig = {
    StateDirectory = "prosody-filer";
    RuntimeDirectory = "prosody-filer";
    RuntimeDirectoryMode = "0750";
  };

  services.prosody-filer = {
    enable = true;
    settings = {
      secret = "plain in line password";
      storeDir = "/var/lib/prosody-filer/uploads/";

      # this option refers to the upload url
      # https://upload.xmpp.${domainName}/upload/
      #                                  <------->
      # do not change this, else 404
      uploadSubDir = "upload/";
    };
  };

Set up nginx virtual host

  services.nginx = {
    clientMaxBodySize = "50m";
    virtualHosts."upload.xmpp.${domainName}" = {
      forceSSL = true;
      useACMEHost = "${domainName}";
      locations."/upload/" = {
        proxyPass = "http://localhost:5050/upload/";
        # config copied from https://github.com/ThomasLeister/prosody-filer/blob/develop/README.md
        extraConfig = ''
            proxy_request_buffering off;

          if ( $request_method = OPTIONS ) {
                  add_header Access-Control-Allow-Origin '*';
                  add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, HEAD';
                  add_header Access-Control-Allow-Headers 'Authorization, Content-Type';
                  add_header Access-Control-Allow-Credentials 'true';
                  add_header Content-Length 0;
                  add_header Content-Type text/plain;
                  return 200;
          }
        '';
      };
    };
  };

Connect filer to Prosody

  services.prosody.extraConfig = ''
    Component "upload.xmpp.${domainName}" "http_upload_external"
    http_upload_external_secret = "should be the same pwd as above";
    http_upload_external_base_url = "https://upload.xmpp.${domainName}/upload/"
    http_upload_external_file_size_limit = 52428800
    -- Grant permissions for users on the '${domainName}' VirtualHost
    parent_host = "${domainName}"
  '';
  services.prosody.virtualHosts."xmpp.${domainName}".extraConfig = ''
    disco_items = {
      { "upload.${domainName}", "http upload" };
    }
  '';

  # mod_http_upload_external is a community module
  # has to be installed via
  # https://modules.prosody.im/mod_http_upload_external.html#installation

  ## Clone Prosody Community Modules Repo
  # cd /var/lib/prosody
  # hg clone https://hg.prosody.im/prosody-modules/ prosody-modules
  # update plugins via hg pull --update
  services.prosody.extraPluginPaths = [
    "/var/lib/prosody/prosody-modules/mod_http_upload_external"
  ];

Final Checkup

Run prosodyctl check to check for local configuration and DNS setup errors.