1 { config, lib, pkgs, ... }:
5 cfg = config.services.prosody;
13 description = "Path to the key file.";
16 # TODO: rename to certificate to match the prosody config
19 description = "Path to the certificate file.";
22 extraOptions = mkOption {
25 description = "Extra SSL configuration options.";
35 description = "URL of the endpoint you want to make discoverable";
37 description = mkOption {
39 description = "A short description of the endpoint you want to advertise";
45 # Required for compliance with https://compliance.conversations.im/about/
49 description = "Allow users to have a roster";
55 description = "Authentication for clients and servers. Recommended if you want to log in.";
61 description = "Add support for secure TLS on c2s/s2s connections";
67 description = "s2s dialback support";
73 description = "Service discovery";
76 # Not essential, but recommended
80 description = "Keep multiple clients in sync";
86 description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
89 cloud_notify = mkOption {
92 description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
98 description = "Enables users to publish their mood, activity, playing music and more";
104 description = "Private XML storage (for room bookmarks, etc.)";
107 blocklist = mkOption {
110 description = "Allow users to block communications with other users";
116 description = "Allow users to set vCards";
119 vcard_legacy = mkOption {
122 description = "Converts users profiles and Avatars between old and new formats";
125 bookmarks = mkOption {
128 description = "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 = "Replies to server version requests";
141 description = "Report how long server has been running";
147 description = "Let others know the time here on this server";
153 description = "Replies to XMPP pings with pongs";
156 register = mkOption {
159 description = "Allow users to register on this server using a client and change passwords";
165 description = "Store messages in an archive and allow users to access it";
171 description = "Allow a client to resume a disconnected session, and prevent message loss";
175 admin_adhoc = mkOption {
178 description = "Allows administration via an XMPP client that supports ad-hoc commands";
181 http_files = mkOption {
184 description = "Serve static files from a directory over HTTP";
190 description = "Enables a file transfer proxy service which clients behind NAT can use";
193 admin_telnet = mkOption {
196 description = "Opens telnet console interface on localhost port 5582";
203 description = "Enable BOSH clients, aka 'Jabber over HTTP'";
206 websocket = mkOption {
209 description = "Enable WebSocket support";
212 # Other specific functionality
216 description = "Enable bandwidth limiting for XMPP connections";
222 description = "Shared roster support";
225 server_contact_info = mkOption {
228 description = "Publish contact information for this service";
231 announce = mkOption {
234 description = "Send announcement to all online users";
240 description = "Welcome users who register accounts";
243 watchregistrations = mkOption {
246 description = "Alert admins of registrations";
252 description = "Send a message to users when they log in";
255 legacyauth = mkOption {
258 description = "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 settingsToLua = prefix: settings: generators.toKeyValue
271 listsAsDuplicateKeys = false;
273 generators.mkKeyValueDefault
275 mkValueString = toLua;
279 (filterAttrs (_k: v: v != null) settings);
281 createSSLOptsStr = o: ''
283 cafile = "/etc/ssl/certs/ca-bundle.crt";
285 certificate = "${o.cert}";
286 ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
294 description = "Domain name of the MUC";
298 description = "The name to return in service discovery responses for the MUC service itself";
299 default = "Prosody Chatrooms";
301 restrictRoomCreation = mkOption {
302 type = types.enum [ true false "admin" "local" ];
304 description = "Restrict room creation to server admins";
306 maxHistoryMessages = mkOption {
309 description = "Specifies a limit on what each room can be configured to keep";
311 roomLocking = mkOption {
315 Enables room locking, which means that a room must be
316 configured before it can be used. Locked rooms are invisible
317 and cannot be entered by anyone but the creator
320 roomLockTimeout = mkOption {
324 Timout after which the room is destroyed or unlocked if not
325 configured, in seconds
328 tombstones = mkOption {
332 When a room is destroyed, it leaves behind a tombstone which
333 prevents the room being entered or recreated. It also allows
334 anyone who was not in the room at the time it was destroyed
335 to learn about it, and to update their bookmarks. Tombstones
336 prevents the case where someone could recreate a previously
337 semi-anonymous room in order to learn the real JIDs of those
338 who often join there.
341 tombstoneExpiry = mkOption {
345 This settings controls how long a tombstone is considered
346 valid. It defaults to 31 days. After this time, the room in
347 question can be created again.
351 vcard_muc = mkOption {
354 description = "Adds the ability to set vCard for Multi User Chat rooms";
357 # Extra parameters. Defaulting to prosody default values.
358 # Adding them explicitly to make them visible from the options
361 # See https://prosody.im/doc/modules/mod_muc for more details.
362 roomDefaultPublic = mkOption {
365 description = "If set, the MUC rooms will be public by default.";
367 roomDefaultMembersOnly = mkOption {
370 description = "If set, the MUC rooms will only be accessible to the members by default.";
372 roomDefaultModerated = mkOption {
375 description = "If set, the MUC rooms will be moderated by default.";
377 roomDefaultPublicJids = mkOption {
380 description = "If set, the MUC rooms will display the public JIDs by default.";
382 roomDefaultChangeSubject = mkOption {
385 description = "If set, the rooms will display the public JIDs by default.";
387 roomDefaultHistoryLength = mkOption {
390 description = "Number of history message sent to participants by default.";
392 roomDefaultLanguage = mkOption {
395 description = "Default room language.";
397 extraConfig = mkOption {
400 description = "Additional MUC specific configuration";
405 uploadHttpOpts = { ... }: {
408 type = types.nullOr types.str;
409 description = "Domain name for the http-upload service";
411 uploadFileSizeLimit = mkOption {
413 default = "50 * 1024 * 1024";
414 description = "Maximum file size, in bytes. Defaults to 50MB.";
416 uploadExpireAfter = mkOption {
418 default = "60 * 60 * 24 * 7";
419 description = "Max age of a file before it gets deleted, in seconds.";
421 userQuota = mkOption {
422 type = types.nullOr types.int;
426 Maximum size of all uploaded files per user, in bytes. There
427 will be no quota if this option is set to null.
430 httpUploadPath = mkOption {
433 Directory where the uploaded files will be stored
434 when the http_upload module is used.
435 By default, uploaded files are put in a sub-directory of the
436 default Prosody storage path (usually /var/lib/prosody).
438 default = "/var/lib/prosody";
443 httpFileShareOpts = { ... }: {
444 freeformType = with types;
445 let atom = oneOf [ int bool str (listOf atom) ]; in
446 attrsOf (nullOr atom);
447 options.domain = mkOption {
448 type = with types; nullOr str;
449 description = "Domain name for a http_file_share service.";
453 vHostOpts = { ... }: {
457 # TODO: require attribute
460 description = "Domain name";
466 description = "Whether to enable the virtual host";
470 type = types.nullOr (types.submodule sslOpts);
472 description = "Paths to SSL files";
475 extraConfig = mkOption {
478 description = "Additional virtual host specific configuration";
498 description = "Whether to enable the prosody server";
501 xmppComplianceSuite = mkOption {
505 The XEP-0423 defines a set of recommended XEPs to implement
506 for a server. It's generally a good idea to implement this
507 set of extensions if you want to provide your users with a
508 good XMPP experience.
510 This NixOS module aims to provide a "advanced server"
511 experience as per defined in the XEP-0423[1] specification.
513 Setting this option to true will prevent you from building a
514 NixOS configuration which won't comply with this standard.
515 You can explicitely decide to ignore this standard if you
516 know what you are doing by setting this option to false.
518 [1] https://xmpp.org/extensions/xep-0423.html
523 type = types.package;
524 description = "Prosody package to use";
525 default = pkgs.prosody;
526 defaultText = literalExpression "pkgs.prosody";
527 example = literalExpression ''
528 pkgs.prosody.override {
529 withExtraLibs = [ pkgs.luaPackages.lpty ];
530 withCommunityModules = [ "auth_external" ];
537 description = "Directory where Prosody stores its data";
538 default = "/var/lib/prosody";
541 disco_items = mkOption {
542 type = types.listOf (types.submodule discoOpts);
544 description = "List of discoverable items you want to advertise.";
550 description = "User account under which prosody runs.";
556 description = "Group account under which prosody runs.";
559 allowRegistration = mkOption {
562 description = "Allow account creation";
565 # HTTP server-related options
566 httpPorts = mkOption {
567 type = types.listOf types.int;
568 description = "Listening HTTP ports list for this service.";
572 httpInterfaces = mkOption {
573 type = types.listOf types.str;
574 default = [ "*" "::" ];
575 description = "Interfaces on which the HTTP server will listen on.";
578 httpsPorts = mkOption {
579 type = types.listOf types.int;
580 description = "Listening HTTPS ports list for this service.";
584 httpsInterfaces = mkOption {
585 type = types.listOf types.str;
586 default = [ "*" "::" ];
587 description = "Interfaces on which the HTTPS server will listen on.";
590 c2sRequireEncryption = mkOption {
594 Force clients to use encrypted connections? This option will
595 prevent clients from authenticating unless they are using encryption.
599 s2sRequireEncryption = mkOption {
603 Force servers to use encrypted connections? This option will
604 prevent servers from authenticating unless they are using encryption.
605 Note that this is different from authentication.
609 s2sSecureAuth = mkOption {
613 Force certificate authentication for server-to-server connections?
614 This provides ideal security, but requires servers you communicate
615 with to support encryption AND present valid, trusted certificates.
616 For more information see https://prosody.im/doc/s2s#security
620 s2sInsecureDomains = mkOption {
621 type = types.listOf types.str;
623 example = [ "insecure.example.com" ];
625 Some servers have invalid or self-signed certificates. You can list
626 remote domains here that will not be required to authenticate using
627 certificates. They will be authenticated using DNS instead, even
628 when s2s_secure_auth is enabled.
632 s2sSecureDomains = mkOption {
633 type = types.listOf types.str;
635 example = [ "jabber.org" ];
637 Even if you leave s2s_secure_auth disabled, you can still require valid
638 certificates for some domains by specifying a list here.
643 modules = moduleOpts;
645 extraModules = mkOption {
646 type = types.listOf types.str;
648 description = "Enable custom modules";
651 extraPluginPaths = mkOption {
652 type = types.listOf types.path;
654 description = "Addtional path in which to look find plugins/modules";
657 uploadHttp = mkOption {
659 Configures the old Prosody builtin HTTP server to handle user uploads.
661 type = types.nullOr (types.submodule uploadHttpOpts);
664 domain = "uploads.my-xmpp-example-host.org";
668 httpFileShare = mkOption {
670 Configures the http_file_share module to handle user uploads.
672 type = types.nullOr (types.submodule httpFileShareOpts);
675 domain = "uploads.my-xmpp-example-host.org";
680 type = types.listOf (types.submodule mucOpts);
683 domain = "conference.my-xmpp-example-host.org";
685 description = "Multi User Chat (MUC) configuration";
688 virtualHosts = mkOption {
690 description = "Define the virtual hosts";
692 type = with types; attrsOf (submodule vHostOpts);
696 domain = "my-xmpp-example-host.org";
703 domain = "localhost";
711 type = types.nullOr (types.submodule sslOpts);
713 description = "Paths to SSL files";
717 type = types.listOf types.str;
719 example = [ "admin1@example.com" "admin2@example.com" ];
720 description = "List of administrators of the current host";
723 authentication = mkOption {
724 type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
725 default = "internal_hashed";
726 example = "internal_plain";
727 description = "Authentication mechanism used for logins.";
730 extraConfig = mkOption {
733 description = "Additional prosody configuration";
740 ###### implementation
742 config = mkIf cfg.enable {
748 Having a server not XEP-0423-compliant might make your XMPP
749 experience terrible. See the NixOS manual for further
752 If you know what you're doing, you can disable this warning by
753 setting config.services.prosody.xmppComplianceSuite to false.
757 assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
759 You need to setup at least a MUC domain to comply with
764 assertion = cfg.uploadHttp != null || cfg.httpFileShare != null || !cfg.xmppComplianceSuite;
766 You need to setup the http_upload or http_file_share modules through
767 config.services.prosody.uploadHttp
768 or config.services.prosody.httpFileShare
769 to comply with XEP-0423.
776 environment.systemPackages = [ cfg.package ];
778 environment.etc."prosody/prosody.cfg.lua".text =
781 optional (cfg.uploadHttp != null)
782 { url = cfg.uploadHttp.domain; description = "HTTP upload endpoint"; } ++
783 optional (cfg.httpFileShare != null)
784 { url = cfg.httpFileShare.domain; description = "HTTP file share endpoint"; };
785 mucDiscoItems = builtins.foldl'
786 (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint"; }] ++ acc)
789 discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
793 pidfile = "/run/prosody/prosody.pid"
797 data_path = "${cfg.dataDir}"
799 ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
802 ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
804 admins = ${toLua cfg.admins}
806 -- we already build with libevent, so we can just enable it for a more performant server
811 ${ lib.concatStringsSep "\n " (lib.mapAttrsToList
812 (name: val: optionalString val "${toLua name};")
814 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
815 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
819 ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
822 allow_registration = ${toLua cfg.allowRegistration}
824 c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
826 s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
828 s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
830 s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
832 s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
834 authentication = ${toLua cfg.authentication}
836 http_interfaces = ${toLua cfg.httpInterfaces}
838 https_interfaces = ${toLua cfg.httpsInterfaces}
840 http_ports = ${toLua cfg.httpPorts}
842 https_ports = ${toLua cfg.httpsPorts}
846 ${lib.concatMapStrings (muc: ''
847 Component ${toLua muc.domain} "muc"
848 modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
849 name = ${toLua muc.name}
850 restrict_room_creation = ${toLua muc.restrictRoomCreation}
851 max_history_messages = ${toLua muc.maxHistoryMessages}
852 muc_room_locking = ${toLua muc.roomLocking}
853 muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
854 muc_tombstones = ${toLua muc.tombstones}
855 muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
856 muc_room_default_public = ${toLua muc.roomDefaultPublic}
857 muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
858 muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
859 muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
860 muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
861 muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
862 muc_room_default_language = ${toLua muc.roomDefaultLanguage}
866 ${ lib.optionalString (cfg.uploadHttp != null) ''
867 Component ${toLua cfg.uploadHttp.domain} "http_upload"
868 http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
869 http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
870 ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
871 http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
874 ${ lib.optionalString (cfg.httpFileShare != null) ''
875 Component ${toLua cfg.httpFileShare.domain} "http_file_share"
876 ${settingsToLua " http_file_share_" (cfg.httpFileShare // { domain = null; })}
879 ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (_n: v: ''
880 VirtualHost "${v.domain}"
881 enabled = ${boolToString v.enabled};
882 ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
884 '') cfg.virtualHosts) }
887 users.users.prosody = mkIf (cfg.user == "prosody") {
888 uid = config.ids.uids.prosody;
889 description = "Prosody user";
892 home = "${cfg.dataDir}";
895 users.groups.prosody = mkIf (cfg.group == "prosody") {
896 gid = config.ids.gids.prosody;
899 systemd.services.prosody = {
900 description = "Prosody XMPP server";
901 after = [ "network-online.target" ];
902 wants = [ "network-online.target" ];
903 wantedBy = [ "multi-user.target" ];
904 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
909 RuntimeDirectory = [ "prosody" ];
910 PIDFile = "/run/prosody/prosody.pid";
911 ExecStart = "${cfg.package}/bin/prosodyctl start";
912 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
914 MemoryDenyWriteExecute = true;
915 PrivateDevices = true;
916 PrivateMounts = true;
918 ProtectControlGroups = true;
920 ProtectHostname = true;
921 ProtectKernelModules = true;
922 ProtectKernelTunables = true;
923 RestrictNamespaces = true;
924 RestrictRealtime = true;
925 RestrictSUIDSGID = true;
930 meta.doc = ./prosody.xml;