1 { config, lib, pkgs, ... }:
5 cfg = config.services.prosody;
13 description = lib.mdDoc "Path to the key file.";
16 # TODO: rename to certificate to match the prosody config
19 description = lib.mdDoc "Path to the certificate file.";
22 extraOptions = mkOption {
25 description = lib.mdDoc "Extra SSL configuration options.";
35 description = lib.mdDoc "URL of the endpoint you want to make discoverable";
37 description = mkOption {
39 description = lib.mdDoc "A short description of the endpoint you want to advertise";
45 # Required for compliance with https://compliance.conversations.im/about/
49 description = lib.mdDoc "Allow users to have a roster";
55 description = lib.mdDoc "Authentication for clients and servers. Recommended if you want to log in.";
61 description = lib.mdDoc "Add support for secure TLS on c2s/s2s connections";
67 description = lib.mdDoc "s2s dialback support";
73 description = lib.mdDoc "Service discovery";
76 # Not essential, but recommended
80 description = lib.mdDoc "Keep multiple clients in sync";
86 description = lib.mdDoc "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
89 cloud_notify = mkOption {
92 description = lib.mdDoc "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
98 description = lib.mdDoc "Enables users to publish their mood, activity, playing music and more";
104 description = lib.mdDoc "Private XML storage (for room bookmarks, etc.)";
107 blocklist = mkOption {
110 description = lib.mdDoc "Allow users to block communications with other users";
116 description = lib.mdDoc "Allow users to set vCards";
119 vcard_legacy = mkOption {
122 description = lib.mdDoc "Converts users profiles and Avatars between old and new formats";
125 bookmarks = mkOption {
128 description = lib.mdDoc "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP";
135 description = lib.mdDoc "Replies to server version requests";
141 description = lib.mdDoc "Report how long server has been running";
147 description = lib.mdDoc "Let others know the time here on this server";
153 description = lib.mdDoc "Replies to XMPP pings with pongs";
156 register = mkOption {
159 description = lib.mdDoc "Allow users to register on this server using a client and change passwords";
165 description = lib.mdDoc "Store messages in an archive and allow users to access it";
171 description = lib.mdDoc "Allow a client to resume a disconnected session, and prevent message loss";
175 admin_adhoc = mkOption {
178 description = lib.mdDoc "Allows administration via an XMPP client that supports ad-hoc commands";
181 http_files = mkOption {
184 description = lib.mdDoc "Serve static files from a directory over HTTP";
190 description = lib.mdDoc "Enables a file transfer proxy service which clients behind NAT can use";
193 admin_telnet = mkOption {
196 description = lib.mdDoc "Opens telnet console interface on localhost port 5582";
203 description = lib.mdDoc "Enable BOSH clients, aka 'Jabber over HTTP'";
206 websocket = mkOption {
209 description = lib.mdDoc "Enable WebSocket support";
212 # Other specific functionality
216 description = lib.mdDoc "Enable bandwidth limiting for XMPP connections";
222 description = lib.mdDoc "Shared roster support";
225 server_contact_info = mkOption {
228 description = lib.mdDoc "Publish contact information for this service";
231 announce = mkOption {
234 description = lib.mdDoc "Send announcement to all online users";
240 description = lib.mdDoc "Welcome users who register accounts";
243 watchregistrations = mkOption {
246 description = lib.mdDoc "Alert admins of registrations";
252 description = lib.mdDoc "Send a message to users when they log in";
255 legacyauth = mkOption {
258 description = lib.mdDoc "Legacy authentication. Only used by some old clients and bots";
263 if builtins.isString x then ''"${x}"''
264 else if builtins.isBool x then boolToString x
265 else if builtins.isInt x then toString x
266 else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
267 else throw "Invalid Lua value";
269 createSSLOptsStr = o: ''
271 cafile = "/etc/ssl/certs/ca-bundle.crt";
273 certificate = "${o.cert}";
274 ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
282 description = lib.mdDoc "Domain name of the MUC";
286 description = lib.mdDoc "The name to return in service discovery responses for the MUC service itself";
287 default = "Prosody Chatrooms";
289 restrictRoomCreation = mkOption {
290 type = types.enum [ true false "admin" "local" ];
292 description = lib.mdDoc "Restrict room creation to server admins";
294 maxHistoryMessages = mkOption {
297 description = lib.mdDoc "Specifies a limit on what each room can be configured to keep";
299 roomLocking = mkOption {
302 description = lib.mdDoc ''
303 Enables room locking, which means that a room must be
304 configured before it can be used. Locked rooms are invisible
305 and cannot be entered by anyone but the creator
308 roomLockTimeout = mkOption {
311 description = lib.mdDoc ''
312 Timout after which the room is destroyed or unlocked if not
313 configured, in seconds
316 tombstones = mkOption {
319 description = lib.mdDoc ''
320 When a room is destroyed, it leaves behind a tombstone which
321 prevents the room being entered or recreated. It also allows
322 anyone who was not in the room at the time it was destroyed
323 to learn about it, and to update their bookmarks. Tombstones
324 prevents the case where someone could recreate a previously
325 semi-anonymous room in order to learn the real JIDs of those
326 who often join there.
329 tombstoneExpiry = mkOption {
332 description = lib.mdDoc ''
333 This settings controls how long a tombstone is considered
334 valid. It defaults to 31 days. After this time, the room in
335 question can be created again.
339 vcard_muc = mkOption {
342 description = lib.mdDoc "Adds the ability to set vCard for Multi User Chat rooms";
345 # Extra parameters. Defaulting to prosody default values.
346 # Adding them explicitly to make them visible from the options
349 # See https://prosody.im/doc/modules/mod_muc for more details.
350 roomDefaultPublic = mkOption {
353 description = lib.mdDoc "If set, the MUC rooms will be public by default.";
355 roomDefaultMembersOnly = mkOption {
358 description = lib.mdDoc "If set, the MUC rooms will only be accessible to the members by default.";
360 roomDefaultModerated = mkOption {
363 description = lib.mdDoc "If set, the MUC rooms will be moderated by default.";
365 roomDefaultPublicJids = mkOption {
368 description = lib.mdDoc "If set, the MUC rooms will display the public JIDs by default.";
370 roomDefaultChangeSubject = mkOption {
373 description = lib.mdDoc "If set, the rooms will display the public JIDs by default.";
375 roomDefaultHistoryLength = mkOption {
378 description = lib.mdDoc "Number of history message sent to participants by default.";
380 roomDefaultLanguage = mkOption {
383 description = lib.mdDoc "Default room language.";
385 extraConfig = mkOption {
388 description = lib.mdDoc "Additional MUC specific configuration";
393 uploadHttpOpts = { ... }: {
396 type = types.nullOr types.str;
397 description = lib.mdDoc "Domain name for the http-upload service";
399 uploadFileSizeLimit = mkOption {
401 default = "50 * 1024 * 1024";
402 description = lib.mdDoc "Maximum file size, in bytes. Defaults to 50MB.";
404 uploadExpireAfter = mkOption {
406 default = "60 * 60 * 24 * 7";
407 description = lib.mdDoc "Max age of a file before it gets deleted, in seconds.";
409 userQuota = mkOption {
410 type = types.nullOr types.int;
413 description = lib.mdDoc ''
414 Maximum size of all uploaded files per user, in bytes. There
415 will be no quota if this option is set to null.
418 httpUploadPath = mkOption {
420 description = lib.mdDoc ''
421 Directory where the uploaded files will be stored. By
422 default, uploaded files are put in a sub-directory of the
423 default Prosody storage path (usually /var/lib/prosody).
425 default = "/var/lib/prosody";
430 vHostOpts = { ... }: {
434 # TODO: require attribute
437 description = lib.mdDoc "Domain name";
443 description = lib.mdDoc "Whether to enable the virtual host";
447 type = types.nullOr (types.submodule sslOpts);
449 description = lib.mdDoc "Paths to SSL files";
452 extraConfig = mkOption {
455 description = lib.mdDoc "Additional virtual host specific configuration";
475 description = lib.mdDoc "Whether to enable the prosody server";
478 xmppComplianceSuite = mkOption {
481 description = lib.mdDoc ''
482 The XEP-0423 defines a set of recommended XEPs to implement
483 for a server. It's generally a good idea to implement this
484 set of extensions if you want to provide your users with a
485 good XMPP experience.
487 This NixOS module aims to provide a "advanced server"
488 experience as per defined in the XEP-0423[1] specification.
490 Setting this option to true will prevent you from building a
491 NixOS configuration which won't comply with this standard.
492 You can explicitely decide to ignore this standard if you
493 know what you are doing by setting this option to false.
495 [1] https://xmpp.org/extensions/xep-0423.html
500 type = types.package;
501 description = lib.mdDoc "Prosody package to use";
502 default = pkgs.prosody;
503 defaultText = literalExpression "pkgs.prosody";
504 example = literalExpression ''
505 pkgs.prosody.override {
506 withExtraLibs = [ pkgs.luaPackages.lpty ];
507 withCommunityModules = [ "auth_external" ];
514 default = "/var/lib/prosody";
515 description = lib.mdDoc ''
516 The prosody home directory used to store all data. If left as the default value
517 this directory will automatically be created before the prosody server starts, otherwise
518 you are responsible for ensuring the directory exists with appropriate ownership
523 disco_items = mkOption {
524 type = types.listOf (types.submodule discoOpts);
526 description = lib.mdDoc "List of discoverable items you want to advertise.";
532 description = lib.mdDoc ''
533 User account under which prosody runs.
536 If left as the default value this user will automatically be created
537 on system activation, otherwise you are responsible for
538 ensuring the user exists before the prosody service starts.
546 description = lib.mdDoc ''
547 Group account under which prosody runs.
550 If left as the default value this group will automatically be created
551 on system activation, otherwise you are responsible for
552 ensuring the group exists before the prosody service starts.
557 allowRegistration = mkOption {
560 description = lib.mdDoc "Allow account creation";
563 # HTTP server-related options
564 httpPorts = mkOption {
565 type = types.listOf types.int;
566 description = lib.mdDoc "Listening HTTP ports list for this service.";
570 httpInterfaces = mkOption {
571 type = types.listOf types.str;
572 default = [ "*" "::" ];
573 description = lib.mdDoc "Interfaces on which the HTTP server will listen on.";
576 httpsPorts = mkOption {
577 type = types.listOf types.int;
578 description = lib.mdDoc "Listening HTTPS ports list for this service.";
582 httpsInterfaces = mkOption {
583 type = types.listOf types.str;
584 default = [ "*" "::" ];
585 description = lib.mdDoc "Interfaces on which the HTTPS server will listen on.";
588 c2sRequireEncryption = mkOption {
591 description = lib.mdDoc ''
592 Force clients to use encrypted connections? This option will
593 prevent clients from authenticating unless they are using encryption.
597 s2sRequireEncryption = mkOption {
600 description = lib.mdDoc ''
601 Force servers to use encrypted connections? This option will
602 prevent servers from authenticating unless they are using encryption.
603 Note that this is different from authentication.
607 s2sSecureAuth = mkOption {
610 description = lib.mdDoc ''
611 Force certificate authentication for server-to-server connections?
612 This provides ideal security, but requires servers you communicate
613 with to support encryption AND present valid, trusted certificates.
614 For more information see https://prosody.im/doc/s2s#security
618 s2sInsecureDomains = mkOption {
619 type = types.listOf types.str;
621 example = [ "insecure.example.com" ];
622 description = lib.mdDoc ''
623 Some servers have invalid or self-signed certificates. You can list
624 remote domains here that will not be required to authenticate using
625 certificates. They will be authenticated using DNS instead, even
626 when s2s_secure_auth is enabled.
630 s2sSecureDomains = mkOption {
631 type = types.listOf types.str;
633 example = [ "jabber.org" ];
634 description = lib.mdDoc ''
635 Even if you leave s2s_secure_auth disabled, you can still require valid
636 certificates for some domains by specifying a list here.
641 modules = moduleOpts;
643 extraModules = mkOption {
644 type = types.listOf types.str;
646 description = lib.mdDoc "Enable custom modules";
649 extraPluginPaths = mkOption {
650 type = types.listOf types.path;
652 description = lib.mdDoc "Addtional path in which to look find plugins/modules";
655 uploadHttp = mkOption {
656 description = lib.mdDoc ''
657 Configures the Prosody builtin HTTP server to handle user uploads.
659 type = types.nullOr (types.submodule uploadHttpOpts);
662 domain = "uploads.my-xmpp-example-host.org";
667 type = types.listOf (types.submodule mucOpts);
670 domain = "conference.my-xmpp-example-host.org";
672 description = lib.mdDoc "Multi User Chat (MUC) configuration";
675 virtualHosts = mkOption {
677 description = lib.mdDoc "Define the virtual hosts";
679 type = with types; attrsOf (submodule vHostOpts);
683 domain = "my-xmpp-example-host.org";
690 domain = "localhost";
698 type = types.nullOr (types.submodule sslOpts);
700 description = lib.mdDoc "Paths to SSL files";
704 type = types.listOf types.str;
706 example = [ "admin1@example.com" "admin2@example.com" ];
707 description = lib.mdDoc "List of administrators of the current host";
710 authentication = mkOption {
711 type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
712 default = "internal_hashed";
713 example = "internal_plain";
714 description = lib.mdDoc "Authentication mechanism used for logins.";
717 extraConfig = mkOption {
720 description = lib.mdDoc "Additional prosody configuration";
727 ###### implementation
729 config = mkIf cfg.enable {
734 Having a server not XEP-0423-compliant might make your XMPP
735 experience terrible. See the NixOS manual for further
738 If you know what you're doing, you can disable this warning by
739 setting config.services.prosody.xmppComplianceSuite to false.
742 { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
744 You need to setup at least a MUC domain to comply with
747 { assertion = cfg.uploadHttp != null || !cfg.xmppComplianceSuite;
749 You need to setup the uploadHttp module through
750 config.services.prosody.uploadHttp to comply with
756 environment.systemPackages = [ cfg.package ];
758 environment.etc."prosody/prosody.cfg.lua".text =
760 httpDiscoItems = if (cfg.uploadHttp != null)
761 then [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}]
763 mucDiscoItems = builtins.foldl'
764 (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
767 discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
770 pidfile = "/run/prosody/prosody.pid"
774 data_path = "${cfg.dataDir}"
776 ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
779 ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
781 admins = ${toLua cfg.admins}
783 -- we already build with libevent, so we can just enable it for a more performant server
788 ${ lib.concatStringsSep "\n " (lib.mapAttrsToList
789 (name: val: optionalString val "${toLua name};")
791 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
792 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
796 ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
799 allow_registration = ${toLua cfg.allowRegistration}
801 c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
803 s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
805 s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
807 s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
809 s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
811 authentication = ${toLua cfg.authentication}
813 http_interfaces = ${toLua cfg.httpInterfaces}
815 https_interfaces = ${toLua cfg.httpsInterfaces}
817 http_ports = ${toLua cfg.httpPorts}
819 https_ports = ${toLua cfg.httpsPorts}
823 ${lib.concatMapStrings (muc: ''
824 Component ${toLua muc.domain} "muc"
825 modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
826 name = ${toLua muc.name}
827 restrict_room_creation = ${toLua muc.restrictRoomCreation}
828 max_history_messages = ${toLua muc.maxHistoryMessages}
829 muc_room_locking = ${toLua muc.roomLocking}
830 muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
831 muc_tombstones = ${toLua muc.tombstones}
832 muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
833 muc_room_default_public = ${toLua muc.roomDefaultPublic}
834 muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
835 muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
836 muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
837 muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
838 muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
839 muc_room_default_language = ${toLua muc.roomDefaultLanguage}
843 ${ lib.optionalString (cfg.uploadHttp != null) ''
844 -- TODO: think about migrating this to mod-http_file_share instead.
845 Component ${toLua cfg.uploadHttp.domain} "http_upload"
846 http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
847 http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
848 ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
849 http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
852 ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
853 VirtualHost "${v.domain}"
854 enabled = ${boolToString v.enabled};
855 ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
857 '') cfg.virtualHosts) }
860 users.users.prosody = mkIf (cfg.user == "prosody") {
861 uid = config.ids.uids.prosody;
862 description = "Prosody user";
867 users.groups.prosody = mkIf (cfg.group == "prosody") {
868 gid = config.ids.gids.prosody;
871 systemd.services.prosody = {
872 description = "Prosody XMPP server";
873 after = [ "network-online.target" ];
874 wants = [ "network-online.target" ];
875 wantedBy = [ "multi-user.target" ];
876 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
877 serviceConfig = mkMerge [
882 RuntimeDirectory = [ "prosody" ];
883 PIDFile = "/run/prosody/prosody.pid";
884 ExecStart = "${cfg.package}/bin/prosodyctl start";
885 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
887 MemoryDenyWriteExecute = true;
888 PrivateDevices = true;
889 PrivateMounts = true;
891 ProtectControlGroups = true;
893 ProtectHostname = true;
894 ProtectKernelModules = true;
895 ProtectKernelTunables = true;
896 RestrictNamespaces = true;
897 RestrictRealtime = true;
898 RestrictSUIDSGID = true;
900 (mkIf (cfg.dataDir == "/var/lib/prosody") {
901 StateDirectory = "prosody";
907 meta.doc = ./prosody.xml;