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.concatMapStringsSep ", " toLua x} }"
267 else throw "Invalid Lua value";
269 settingsToLua = prefix: settings: generators.toKeyValue {
270 listsAsDuplicateKeys = false;
271 mkKeyValue = k: generators.mkKeyValueDefault {
272 mkValueString = toLua;
273 } " = " (prefix + k);
274 } (filterAttrs (k: v: v != null) settings);
276 createSSLOptsStr = o: ''
278 cafile = "/etc/ssl/certs/ca-bundle.crt";
280 certificate = "${o.cert}";
281 ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
289 description = "Domain name of the MUC";
293 description = "The name to return in service discovery responses for the MUC service itself";
294 default = "Prosody Chatrooms";
296 restrictRoomCreation = mkOption {
297 type = types.enum [ true false "admin" "local" ];
299 description = "Restrict room creation to server admins";
301 maxHistoryMessages = mkOption {
304 description = "Specifies a limit on what each room can be configured to keep";
306 roomLocking = mkOption {
310 Enables room locking, which means that a room must be
311 configured before it can be used. Locked rooms are invisible
312 and cannot be entered by anyone but the creator
315 roomLockTimeout = mkOption {
319 Timeout after which the room is destroyed or unlocked if not
320 configured, in seconds
323 tombstones = mkOption {
327 When a room is destroyed, it leaves behind a tombstone which
328 prevents the room being entered or recreated. It also allows
329 anyone who was not in the room at the time it was destroyed
330 to learn about it, and to update their bookmarks. Tombstones
331 prevents the case where someone could recreate a previously
332 semi-anonymous room in order to learn the real JIDs of those
333 who often join there.
336 tombstoneExpiry = mkOption {
340 This settings controls how long a tombstone is considered
341 valid. It defaults to 31 days. After this time, the room in
342 question can be created again.
346 vcard_muc = mkOption {
349 description = "Adds the ability to set vCard for Multi User Chat rooms";
352 # Extra parameters. Defaulting to prosody default values.
353 # Adding them explicitly to make them visible from the options
356 # See https://prosody.im/doc/modules/mod_muc for more details.
357 roomDefaultPublic = mkOption {
360 description = "If set, the MUC rooms will be public by default.";
362 roomDefaultMembersOnly = mkOption {
365 description = "If set, the MUC rooms will only be accessible to the members by default.";
367 roomDefaultModerated = mkOption {
370 description = "If set, the MUC rooms will be moderated by default.";
372 roomDefaultPublicJids = mkOption {
375 description = "If set, the MUC rooms will display the public JIDs by default.";
377 roomDefaultChangeSubject = mkOption {
380 description = "If set, the rooms will display the public JIDs by default.";
382 roomDefaultHistoryLength = mkOption {
385 description = "Number of history message sent to participants by default.";
387 roomDefaultLanguage = mkOption {
390 description = "Default room language.";
392 extraConfig = mkOption {
395 description = "Additional MUC specific configuration";
400 uploadHttpOpts = { ... }: {
403 type = types.nullOr types.str;
404 description = "Domain name for the http-upload service";
406 uploadFileSizeLimit = mkOption {
408 default = "50 * 1024 * 1024";
409 description = "Maximum file size, in bytes. Defaults to 50MB.";
411 uploadExpireAfter = mkOption {
413 default = "60 * 60 * 24 * 7";
414 description = "Max age of a file before it gets deleted, in seconds.";
416 userQuota = mkOption {
417 type = types.nullOr types.int;
421 Maximum size of all uploaded files per user, in bytes. There
422 will be no quota if this option is set to null.
425 httpUploadPath = mkOption {
428 Directory where the uploaded files will be stored when the http_upload module is used.
429 By default, uploaded files are put in a sub-directory of the default Prosody storage path (usually /var/lib/prosody).
431 default = "/var/lib/prosody";
436 httpFileShareOpts = { ... }: {
437 freeformType = with types;
438 let atom = oneOf [ int bool str (listOf atom) ]; in
439 attrsOf (nullOr atom) // {
440 description = "int, bool, string or list of them";
442 options.domain = mkOption {
443 type = with types; nullOr str;
444 description = "Domain name for a http_file_share service.";
448 vHostOpts = { ... }: {
452 # TODO: require attribute
455 description = "Domain name";
461 description = "Whether to enable the virtual host";
465 type = types.nullOr (types.submodule sslOpts);
467 description = "Paths to SSL files";
470 extraConfig = mkOption {
473 description = "Additional virtual host specific configuration";
493 description = "Whether to enable the prosody server";
496 xmppComplianceSuite = mkOption {
500 The XEP-0423 defines a set of recommended XEPs to implement
501 for a server. It's generally a good idea to implement this
502 set of extensions if you want to provide your users with a
503 good XMPP experience.
505 This NixOS module aims to provide a "advanced server"
506 experience as per defined in the XEP-0423[1] specification.
508 Setting this option to true will prevent you from building a
509 NixOS configuration which won't comply with this standard.
510 You can explicitly decide to ignore this standard if you
511 know what you are doing by setting this option to false.
513 [1] https://xmpp.org/extensions/xep-0423.html
517 package = mkPackageOption pkgs "prosody" {
519 pkgs.prosody.override {
520 withExtraLibs = [ pkgs.luaPackages.lpty ];
521 withCommunityModules = [ "auth_external" ];
528 default = "/var/lib/prosody";
530 The prosody home directory used to store all data. If left as the default value
531 this directory will automatically be created before the prosody server starts, otherwise
532 you are responsible for ensuring the directory exists with appropriate ownership
537 disco_items = mkOption {
538 type = types.listOf (types.submodule discoOpts);
540 description = "List of discoverable items you want to advertise.";
547 User account under which prosody runs.
550 If left as the default value this user will automatically be created
551 on system activation, otherwise you are responsible for
552 ensuring the user exists before the prosody service starts.
561 Group account under which prosody runs.
564 If left as the default value this group will automatically be created
565 on system activation, otherwise you are responsible for
566 ensuring the group exists before the prosody service starts.
571 allowRegistration = mkOption {
574 description = "Allow account creation";
577 # HTTP server-related options
578 httpPorts = mkOption {
579 type = types.listOf types.int;
580 description = "Listening HTTP ports list for this service.";
584 httpInterfaces = mkOption {
585 type = types.listOf types.str;
586 default = [ "*" "::" ];
587 description = "Interfaces on which the HTTP server will listen on.";
590 httpsPorts = mkOption {
591 type = types.listOf types.int;
592 description = "Listening HTTPS ports list for this service.";
596 httpsInterfaces = mkOption {
597 type = types.listOf types.str;
598 default = [ "*" "::" ];
599 description = "Interfaces on which the HTTPS server will listen on.";
602 c2sRequireEncryption = mkOption {
606 Force clients to use encrypted connections? This option will
607 prevent clients from authenticating unless they are using encryption.
611 s2sRequireEncryption = mkOption {
615 Force servers to use encrypted connections? This option will
616 prevent servers from authenticating unless they are using encryption.
617 Note that this is different from authentication.
621 s2sSecureAuth = mkOption {
625 Force certificate authentication for server-to-server connections?
626 This provides ideal security, but requires servers you communicate
627 with to support encryption AND present valid, trusted certificates.
628 For more information see https://prosody.im/doc/s2s#security
632 s2sInsecureDomains = mkOption {
633 type = types.listOf types.str;
635 example = [ "insecure.example.com" ];
637 Some servers have invalid or self-signed certificates. You can list
638 remote domains here that will not be required to authenticate using
639 certificates. They will be authenticated using DNS instead, even
640 when s2s_secure_auth is enabled.
644 s2sSecureDomains = mkOption {
645 type = types.listOf types.str;
647 example = [ "jabber.org" ];
649 Even if you leave s2s_secure_auth disabled, you can still require valid
650 certificates for some domains by specifying a list here.
655 modules = moduleOpts;
657 extraModules = mkOption {
658 type = types.listOf types.str;
660 description = "Enable custom modules";
663 extraPluginPaths = mkOption {
664 type = types.listOf types.path;
666 description = "Additional path in which to look find plugins/modules";
669 uploadHttp = mkOption {
671 Configures the old Prosody builtin HTTP server to handle user uploads.
673 type = types.nullOr (types.submodule uploadHttpOpts);
676 domain = "uploads.my-xmpp-example-host.org";
680 httpFileShare = mkOption {
682 Configures the http_file_share module to handle user uploads.
684 type = types.nullOr (types.submodule httpFileShareOpts);
687 domain = "uploads.my-xmpp-example-host.org";
692 type = types.listOf (types.submodule mucOpts);
695 domain = "conference.my-xmpp-example-host.org";
697 description = "Multi User Chat (MUC) configuration";
700 virtualHosts = mkOption {
702 description = "Define the virtual hosts";
704 type = with types; attrsOf (submodule vHostOpts);
708 domain = "my-xmpp-example-host.org";
715 domain = "localhost";
723 type = types.nullOr (types.submodule sslOpts);
725 description = "Paths to SSL files";
729 type = types.listOf types.str;
731 example = [ "admin1@example.com" "admin2@example.com" ];
732 description = "List of administrators of the current host";
735 authentication = mkOption {
736 type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
737 default = "internal_hashed";
738 example = "internal_plain";
739 description = "Authentication mechanism used for logins.";
742 extraConfig = mkOption {
745 description = "Additional prosody configuration";
750 default = ''"*syslog"'';
751 description = "Logging configuration. See [](https://prosody.im/doc/logging) for more details";
754 { min = "warn"; to = "*syslog"; };
763 ###### implementation
765 config = mkIf cfg.enable {
770 Having a server not XEP-0423-compliant might make your XMPP
771 experience terrible. See the NixOS manual for further
774 If you know what you're doing, you can disable this warning by
775 setting config.services.prosody.xmppComplianceSuite to false.
778 { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
780 You need to setup at least a MUC domain to comply with
783 { assertion = cfg.uploadHttp != null || cfg.httpFileShare != null || !cfg.xmppComplianceSuite;
785 You need to setup the http_upload or http_file_share modules through config.services.prosody.uploadHttp
786 or config.services.prosody.httpFileShare to comply with XEP-0423.
791 environment.systemPackages = [ cfg.package ];
793 environment.etc."prosody/prosody.cfg.lua".text =
795 httpDiscoItems = optional (cfg.uploadHttp != null) {
796 url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";
797 } ++ optional (cfg.httpFileShare != null) {
798 url = cfg.httpFileShare.domain; description = "HTTP file share endpoint";
800 mucDiscoItems = builtins.foldl'
801 (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
804 discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
807 pidfile = "/run/prosody/prosody.pid"
811 data_path = "${cfg.dataDir}"
813 ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
816 ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
818 admins = ${toLua cfg.admins}
822 ${ lib.concatStringsSep "\n " (lib.mapAttrsToList
823 (name: val: optionalString val "${toLua name};")
825 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
826 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
830 ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
833 allow_registration = ${toLua cfg.allowRegistration}
835 c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
837 s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
839 s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
841 s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
843 s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
845 authentication = ${toLua cfg.authentication}
847 http_interfaces = ${toLua cfg.httpInterfaces}
849 https_interfaces = ${toLua cfg.httpsInterfaces}
851 http_ports = ${toLua cfg.httpPorts}
853 https_ports = ${toLua cfg.httpsPorts}
857 ${lib.concatMapStrings (muc: ''
858 Component ${toLua muc.domain} "muc"
859 modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
860 name = ${toLua muc.name}
861 restrict_room_creation = ${toLua muc.restrictRoomCreation}
862 max_history_messages = ${toLua muc.maxHistoryMessages}
863 muc_room_locking = ${toLua muc.roomLocking}
864 muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
865 muc_tombstones = ${toLua muc.tombstones}
866 muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
867 muc_room_default_public = ${toLua muc.roomDefaultPublic}
868 muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
869 muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
870 muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
871 muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
872 muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
873 muc_room_default_language = ${toLua muc.roomDefaultLanguage}
877 ${ lib.optionalString (cfg.uploadHttp != null) ''
878 Component ${toLua cfg.uploadHttp.domain} "http_upload"
879 http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
880 http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
881 ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
882 http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
885 ${lib.optionalString (cfg.httpFileShare != null) ''
886 Component ${toLua cfg.httpFileShare.domain} "http_file_share"
887 ${settingsToLua " http_file_share_" (cfg.httpFileShare // { domain = null; })}
890 ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
891 VirtualHost "${v.domain}"
892 enabled = ${boolToString v.enabled};
893 ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
895 '') cfg.virtualHosts) }
898 users.users.prosody = mkIf (cfg.user == "prosody") {
899 uid = config.ids.uids.prosody;
900 description = "Prosody user";
905 users.groups.prosody = mkIf (cfg.group == "prosody") {
906 gid = config.ids.gids.prosody;
909 systemd.services.prosody = {
910 description = "Prosody XMPP server";
911 after = [ "network-online.target" ];
912 wants = [ "network-online.target" ];
913 wantedBy = [ "multi-user.target" ];
914 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
915 serviceConfig = mkMerge [
920 RuntimeDirectory = [ "prosody" ];
921 PIDFile = "/run/prosody/prosody.pid";
922 ExecStart = "${cfg.package}/bin/prosodyctl start";
923 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
925 MemoryDenyWriteExecute = true;
926 PrivateDevices = true;
927 PrivateMounts = true;
929 ProtectControlGroups = true;
931 ProtectHostname = true;
932 ProtectKernelModules = true;
933 ProtectKernelTunables = true;
934 RestrictNamespaces = true;
935 RestrictRealtime = true;
936 RestrictSUIDSGID = true;
938 (mkIf (cfg.dataDir == "/var/lib/prosody") {
939 StateDirectory = "prosody";
946 meta.doc = ./prosody.md;