1 { options, config, pkgs, lib, ... }:
3 inherit (lib) isAttrs isBool isList isInt isString;
4 inherit (lib) mkChangedOptionModule mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkRenamedOptionModule;
5 inherit (lib) all any attrValues boolToString concatStringsSep elem filterAttrs genAttrs hasPrefix listToAttrs literalExpression mapAttrsToList mdDoc optionals optionalAttrs optionalString types unique;
8 cfg = config.services.prosody;
10 luaType = with types; let
21 description = "Lua value";
26 sslOption = mkOption {
27 type = with types; nullOr (submodule {
28 freeformType = luaType;
33 description = mdDoc ''
34 Path to your private key file.
38 certificate = mkOption {
40 description = mdDoc ''
41 Path to your certificate file.
47 default = "/etc/ssl/certs/ca-bundle.crt";
48 description = mdDoc ''
49 Path to a file containing root certificates that you wish Prosody to trust.
56 description = mdDoc ''
57 Advanced SSL/TLS configuration, as described by <https://prosody.im/doc/advanced_ssl_config>.
60 Prosody passes the contents of the `ssl` option from the config file almost directly to LuaSec, the
61 library used for SSL/TLS support in Prosody. LuaSec accepts a range of options here, mostly things
62 that it passes directly to OpenSSL. It is recommended to leave Prosody's defaults in most cases, unless
63 you know what you are doing.
64 You could easily reduce security or introduce unnecessary compatibility issues with clients and other servers!
69 componentsOption = mkOption {
70 type = with types; attrsOf (submodule ({ name, config, ... }: {
73 type = with types; nullOr str;
75 description = mdDoc ''
76 The name of the plugin you wish to use for the component if internal, or `null` if the
77 component is an external component.
84 description = mdDoc ''
85 Values specified here are applied to a specific component. Refer to <https://prosody.im/doc/components>
86 for additional details.
90 extraConfig = mkOption {
93 description = mdDoc ''
94 Additional component specific configuration.
100 config.settings = mkMerge [
101 (mkIf (config.module == "http_upload") {
102 http_upload_path = mkIf (config.module == "http_upload_path") cfg.dataDir;
104 (mkIf (config.module == "muc") {
105 modules_enabled = [ "muc_mam" ];
110 description = mdDoc ''
111 Components are extra services on a server which are available to clients, usually on a subdomain
112 of the main server (such as mycomponent.example.com). Example components might be chatroom
113 servers, user directories, or gateways to other protocols.
115 See <https://prosody.im/doc/components> for details.
119 acmeHosts = unique (mapAttrsToList (domain: hostOpts: hostOpts.useACMEHost) (filterAttrs (_: v: v.useACMEHost != null) cfg.virtualHosts));
123 (mkRenamedOptionModule [ "services" "prosody" "allowRegistration" ] [ "services" "prosody" "settings" "allow_registration" ])
124 (mkRenamedOptionModule [ "services" "prosody" "httpPorts" ] [ "services" "prosody" "settings" "http_ports" ])
125 (mkRenamedOptionModule [ "services" "prosody" "httpInterfaces" ] [ "services" "prosody" "settings" "http_interfaces" ])
126 (mkRenamedOptionModule [ "services" "prosody" "httpsPorts" ] [ "services" "prosody" "settings" "https_ports" ])
127 (mkRenamedOptionModule [ "services" "prosody" "httpsInterfaces" ] [ "services" "prosody" "settings" "https_interfaces" ])
128 (mkRenamedOptionModule [ "services" "prosody" "c2sRequireEncryption" ] [ "services" "prosody" "settings" "c2s_require_encryption" ])
129 (mkRenamedOptionModule [ "services" "prosody" "s2sRequireEncryption" ] [ "services" "prosody" "settings" "s2s_require_encryption" ])
130 (mkRenamedOptionModule [ "services" "prosody" "s2sSecureAuth" ] [ "services" "prosody" "settings" "s2s_secure_auth" ])
131 (mkRenamedOptionModule [ "services" "prosody" "s2sInsecureDomains" ] [ "services" "prosody" "settings" "s2s_insecure_domains" ])
132 (mkRenamedOptionModule [ "services" "prosody" "s2sSecureDomains" ] [ "services" "prosody" "settings" "s2s_secure_domains" ])
133 (mkRenamedOptionModule [ "services" "prosody" "extraModules" ] [ "services" "prosody" "settings" "modules_enabled" ])
134 (mkRenamedOptionModule [ "services" "prosody" "extraPluginPaths" ] [ "services" "prosody" "settings" "plugin_paths" ])
135 (mkRenamedOptionModule [ "services" "prosody" "ssl" "key" ] [ "services" "prosody" "settings" "ssl" "key" ])
136 (mkRenamedOptionModule [ "services" "prosody" "ssl" "cert" ] [ "services" "prosody" "settings" "ssl" "certificate" ])
137 (mkRenamedOptionModule [ "services" "prosody" "ssl" "extraOptions" ] [ "services" "prosody" "settings" "ssl" ])
138 (mkRenamedOptionModule [ "services" "prosody" "admins" ] [ "services" "prosody" "settings" "admins" ])
139 (mkRenamedOptionModule [ "services" "prosody" "authentication" ] [ "services" "prosody" "settings" "authentication" ])
140 (mkChangedOptionModule [ "services" "prosody" "disco_items" ] [ "services" "prosody" "settings" "disco_items" ] (config:
141 map ({ url, description }: [ url description ]) config.services.prosody.disco_items
143 (mkChangedOptionModule [ "services" "prosody" "uploadHttp" ] [ "services" "prosody" "components" ] (config:
145 cfg = config.services.prosody;
148 ${cfg.uploadHttp.domain} = {
149 module = "http_upload";
150 # these values are to preserve compatibility with this module pre nixos 23.11
152 http_upload_file_size_limit = { __compat = true; value = cfg.uploadHttp.uploadFileSizeLimit or "50 * 1024 * 1024"; };
153 http_upload_expire_after = { __compat = true; value = cfg.uploadHttp.uploadExpireAfter or "60 * 60 * 24 * 7"; };
154 http_upload_path = cfg.uploadHttp.httpUploadPath or "/var/lib/prosody";
155 } // optionalAttrs (cfg.uploadHttp ? userQuota) {
156 http_upload_quota = cfg.uploadHttp.userQuota;
161 (mkChangedOptionModule [ "services" "prosody" "muc" ] [ "services" "prosody" "components" ] (config:
167 extraConfig = muc.extraConfig or "";
168 # these values are to preserve compatibility with this module pre nixos 23.11
170 modules_enabled = optionals (muc.vcard_muc or true) [ "vcard_muc" ];
171 name = muc.name or "Prosody Chatrooms";
172 restrict_room_creation = muc.restrictRoomCreation or "local";
173 max_history_messages = muc.maxHistoryMessages or 20;
174 muc_room_locking = muc.roomLocking or true;
175 muc_room_lock_timeout = muc.roomLockTimeout or 300;
176 muc_tombstones = muc.tombstones or true;
177 muc_tombstone_expiry = muc.tombstoneExpiry or 2678400;
178 muc_room_default_public = muc.roomDefaultPublic or true;
179 muc_room_default_members_only = muc.roomDefaultMembersOnly or false;
180 muc_room_default_moderated = muc.roomDefaultModerated or false;
181 muc_room_default_public_jids = muc.roomDefaultPublicJids or false;
182 muc_room_default_change_subject = muc.roomDefaultChangeSubject or false;
183 muc_room_default_history_length = muc.roomDefaultHistoryLength or 20;
184 muc_room_default_language = muc.roomDefaultLanguage or "en";
188 config.services.prosody.muc)
192 options.services.prosody = {
193 enable = mkEnableOption "Prosody, the modern XMPP communication server";
195 xmppComplianceSuite = mkOption {
198 description = mdDoc ''
199 The XEP-0423 defines a set of recommended XEPs to implement
200 for a server. It's generally a good idea to implement this
201 set of extensions if you want to provide your users with a
202 good XMPP experience.
204 This NixOS module aims to provide an "advanced server"
205 experience as per defined in the [XEP-0423](https://xmpp.org/extensions/xep-0423.html) specification.
207 Setting this option to `true` will prevent you from building a
208 NixOS configuration which won't comply with this standard.
209 You can explicitly decide to ignore this standard if you
210 know what you are doing by setting this option to `false`.
215 type = types.package;
216 description = mdDoc "Prosody package to use.";
217 default = pkgs.prosody;
218 defaultText = literalExpression "pkgs.prosody";
219 example = literalExpression ''
220 pkgs.prosody.override {
221 withExtraLibs = [ pkgs.luaPackages.lpty ];
222 withCommunityModules = [ "auth_external" ];
229 default = "/var/lib/prosody";
230 description = mdDoc ''
231 The Prosody home directory used to store all data.
234 If left as the default value this directory will automatically be created
235 before the Prosody server starts, otherwise you are responsible for
236 ensuring the directory exists with appropriate ownership and permissions.
244 description = mdDoc ''
245 User account under which Prosody runs.
248 If left as the default value this user will automatically be created
249 on system activation, otherwise you are responsible for
250 ensuring the user exists before the Prosody service starts.
258 description = mdDoc ''
259 Group account under which Prosody runs.
262 If left as the default value this group will automatically be created
263 on system activation, otherwise you are responsible for
264 ensuring the group exists before the Prosody service starts.
269 environmentFile = mkOption {
270 type = with types; nullOr path;
272 description = mdDoc ''
273 File which may contain secrets in the format of an EnvironmentFile as described
274 by systemd.exec(5). Variables here can be used in the Prosody configuration
275 file by prefixing the environment variable name with `ENV_` in the configuration.
278 Environment variables in this file should *not* begin with an `ENV_` prefix, only
279 when referenced in the Prosody configuration file.
284 openFirewall = mkOption {
287 description = mdDoc ''
288 Whether to automatically open ports specified by the following options:
290 - {option}`services.prosody.settings.c2s_ports`
291 - {option}`services.prosody.settings.s2s_ports`
292 - {option}`services.prosody.settings.https_ports`
296 settings = mkOption {
297 type = types.submodule {
298 freeformType = luaType;
301 modules_enabled = mkOption {
302 type = with types; listOf str;
305 description = mdDoc ''
306 List of modules to load for all virtual hosts.
310 modules_disabled = mkOption {
311 type = with types; listOf str;
314 description = mdDoc ''
315 Allows you to disable the loading of a list of modules for all virtual hosts
316 if those modules are set in the global settings.
325 description = mdDoc ''
326 Values specified here are applied to the whole server, and are the default for all
327 virtual hosts. Refer to <https://prosody.im/doc/configure> for additional details.
330 It's also possible to refer to environment variables (defined in
331 [services.prosody.environmentFile](#opt-services.prosody.environmentFile)) using the
332 syntax `"ENV_VARIABLE_NAME"` where the environment file contains a `VARIABLE_NAME` entry.
335 example = literalExpression ''
337 modules_enabled = [ "turn_external" ];
338 turn_external_host = "turn.example.com";
339 turn_external_port = 3478;
340 turn_external_secret = "ENV_TURN_EXTERNAL_SECRET";
345 components = componentsOption;
347 virtualHosts = let config' = config; in mkOption {
348 type = with types; attrsOf (submodule ({ name, config, ... }: {
353 description = mdDoc "Domain name.";
356 useACMEHost = mkOption {
357 type = with types; nullOr str;
359 description = mdDoc ''
360 A host of an existing Let's Encrypt certificate to use.
363 Note that this option does not create any certificates, nor it does add subdomains to existing
364 ones – you will need to create them manually using [](#opt-security.acme.certs).
369 components = componentsOption;
371 settings = mkOption {
372 type = types.submodule {
373 freeformType = luaType;
379 description = mdDoc ''
380 Specifies whether this host is enabled or not. Disabled hosts are not loaded and
381 do not accept connections while Prosody is running.
385 modules_enabled = mkOption {
386 type = with types; listOf str;
389 description = mdDoc ''
390 List of modules to load for the virtual host.
394 modules_disabled = mkOption {
395 type = with types; listOf str;
398 description = mdDoc ''
399 Allows you to disable the loading of a list of modules for a particular host.
407 description = mdDoc ''
408 Values specified here are applied to a specific virtual host and will override values set
409 in the global [settings](#opt-services.prosody.settings) option. Refer to <https://prosody.im/doc/configure>
410 for additional details.
414 extraConfig = mkOption {
417 description = mdDoc ''
418 Additional virtual host specific configuration.
422 # options to preserve compatibility with this module pre nixos 23.11
425 type = with types; nullOr bool;
427 description = mdDoc "Whether to enable the virtual host.";
431 type = types.nullOr (types.submodule {
435 description = lib.mdDoc "Path to the key file.";
440 description = lib.mdDoc "Path to the certificate file.";
443 extraOptions = mkOption {
446 description = lib.mdDoc "Extra SSL configuration options.";
451 description = mdDoc "Paths to SSL files.";
458 (mkIf (config.ssl != null) ({
459 key = config.ssl.key;
460 certificate = config.ssl.cert;
461 } // config.ssl.extraOptions))
462 (mkIf (config.useACMEHost != null) {
463 key = "${config'.security.acme.certs.${config.useACMEHost}.directory}/key.pem";
464 certificate = "${config'.security.acme.certs.${config.useACMEHost}.directory}/fullchain.pem";
469 mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") config.components) ++
470 mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") config.components)
473 enabled = mkIf (config.enabled != null) config.enabled;
479 description = mdDoc ''
480 A host in Prosody is a domain on which user accounts can be created. For example if you want your users to have addresses
481 like `john.smith@example.com` then you need to add a host `example.com`.
483 example = literalExpression ''
486 useACMEHost = "example.net";
488 admins = [ "admin1@example.net" "admin2@example.net" ];
489 c2s_require_encryption = true;
499 extraConfig = mkOption {
502 description = mdDoc ''
503 Additional prosody configuration.
507 # options to preserve compatibility with this module pre nixos 23.11
510 type = types.attrsOf types.bool;
512 description = mdDoc ''
517 config = mkIf cfg.enable {
521 hasAnyModules = mods: b: any (e: elem e.module mods) (attrValues b.components);
522 virtualHosts = filterAttrs (_: v: v.settings.enabled) cfg.virtualHosts;
524 # ensure a given module (e.g. muc or http_upload) is applied or available to every virtual host
525 mkAssertion = modules: message: {
526 assertion = cfg.xmppComplianceSuite -> hasAnyModules modules cfg || all (hasAnyModules modules) (attrValues virtualHosts);
527 message = message + ''
529 Having a server not XEP-0423-compliant might make your XMPP
530 experience terrible. See the NixOS manual for further
533 If you know what you're doing, you can disable this warning by
534 setting config.services.prosody.xmppComplianceSuite to false.
539 (mkAssertion [ "muc" ] ''
540 You need to setup at least a MUC domain to comply with
544 (mkAssertion [ "http_upload" "http_file_share" ] ''
545 You need to setup the http_upload or http_file_share module through
546 config.services.prosody.components to comply with
552 optionals (cfg.modules != { }) [ "The option `services.prosody.modules' has been and split into two separate options: `services.prosody.settings.modules_enabled' and `services.prosody.settings.modules_disabled'." ] ++
553 mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.enabled' has been renamed to `services.prosody.virtualHosts.${k}.settings.enabled'.") (filterAttrs (_: v: v.enabled != null) cfg.virtualHosts) ++
554 mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.ssl' has been renamed to `services.prosody.virtualHosts.${k}.settings.ssl'.") (filterAttrs (_: v: v.ssl != null) cfg.virtualHosts)
557 services.prosody.settings = {
558 log = mkDefault "*syslog";
559 data_path = mkForce cfg.dataDir;
560 network_backend = "event";
561 pidfile = "/run/prosody/prosody.pid";
563 authentication = mkDefault "internal_hashed";
564 reload_modules = mkIf (acmeHosts != [ ]) [ "tls" ];
567 # required for compliance with https://compliance.conversations.im/about/
574 # not essential, but recommended
597 ] ++ cfg.package.communityModules
598 ++ mapAttrsToList (k: _: k) (filterAttrs (_: v: v == true) cfg.modules);
600 modules_disabled = mapAttrsToList (k: _: k) (filterAttrs (_: v: v == false) cfg.modules);
603 mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") cfg.components) ++
604 mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") cfg.components)
607 # mod_tls configuration
608 c2s_require_encryption = mkDefault true;
609 s2s_require_encryption = mkDefault true;
611 # upstream defaults - useful for `services.prosody.openFirewall` logic
612 c2s_ports = mkDefault [ 5222 ];
613 s2s_ports = mkDefault [ 5269 ];
614 https_ports = mkDefault [ 5281 ];
616 # proxy65_ports = [ 5000 ];
619 environment.systemPackages = [ cfg.package ];
620 environment.etc."prosody/prosody.cfg.lua".text =
622 toFormat = attrs: concatStringsSep "\n" (mapAttrsToList (k: v: ''${k} = ${toStr v}'') (filterAttrs (_: v: v != null) attrs));
626 # prosody will directly pass environment variables into its configuration file which have `ENV_` as a prefix
627 if hasPrefix "ENV_" v then v else ''"${toString v}"''
628 else if isBool v then boolToString v
629 else if isInt v then toString v
630 else if isList v then ''{ ${concatStringsSep ", " (map (n: toStr n) v)} }''
631 else if isAttrs v then
632 if v ? __compat then v.value else ''{ ${concatStringsSep ", " (mapAttrsToList (a: b: ''["${a}"] = ${toStr b}'') v)} }''
633 else throw "Invalid Lua value";
635 componentsToStr = components:
636 concatStringsSep "\n" (
640 Component "${domain}" ${optionalString (component.module != null) "\"${component.module}\""}
641 ${toFormat component.settings}
642 ${component.extraConfig}
648 virtualHostsToStr = virtualHosts:
649 concatStringsSep "\n" (
653 VirtualHost "${virtualHost.domain}"
654 ${toFormat virtualHost.settings}
655 ${virtualHost.extraConfig}
657 ${componentsToStr virtualHost.components}
664 ${toFormat cfg.settings}
667 ${componentsToStr cfg.components}
668 ${virtualHostsToStr cfg.virtualHosts}
671 systemd.services.prosody = {
672 description = "Prosody XMPP server";
673 wantedBy = [ "multi-user.target" ];
674 before = map (domain: "acme-${domain}.service") acmeHosts;
675 after = [ "network-online.target" ] ++ map (domain: "acme-selfsigned-${domain}.service") acmeHosts;
676 wants = [ "network-online.target" ] ++ map (domain: "acme-finished-${domain}.target") acmeHosts;
678 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
679 serviceConfig = mkMerge [
684 PIDFile = cfg.settings.pidfile;
685 ExecStart = "${cfg.package}/bin/prosodyctl start";
686 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
687 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
689 MemoryDenyWriteExecute = true;
690 PrivateDevices = true;
691 PrivateMounts = true;
693 ProtectControlGroups = true;
695 ProtectHostname = true;
696 ProtectKernelModules = true;
697 ProtectKernelTunables = true;
698 RestrictNamespaces = true;
699 RestrictRealtime = true;
700 RestrictSUIDSGID = true;
702 (mkIf (cfg.dataDir == "/var/lib/prosody") {
703 StateDirectory = "prosody";
704 StateDirectoryMode = "0750";
706 (mkIf (cfg.settings.pidfile == "/run/prosody/prosody.pid") {
707 RuntimeDirectory = [ "prosody" ];
712 security.acme.certs = genAttrs acmeHosts (_: {
713 reloadServices = [ "prosody.service" ];
716 networking.firewall = optionalAttrs cfg.openFirewall {
717 allowedTCPPorts = cfg.settings.c2s_ports
718 ++ cfg.settings.s2s_ports
719 ++ cfg.settings.https_ports
720 # TODO: cfg.settings.proxy65_ports
724 users.users.prosody = mkIf (cfg.user == "prosody") {
725 uid = config.ids.uids.prosody;
726 description = "Prosody user";
731 users.groups.prosody = mkIf (cfg.group == "prosody") {
732 gid = config.ids.gids.prosody;
736 meta.doc = ./prosody.md;