-{ config, lib, pkgs, ... }:
-
-with lib;
+{ options, config, pkgs, lib, ... }:
let
- cfg = config.services.prosody;
+ inherit (lib) isAttrs isBool isList isInt isString;
+ inherit (lib) mkChangedOptionModule mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkRenamedOptionModule;
+ inherit (lib) all any attrValues boolToString concatStringsSep elem filterAttrs genAttrs hasPrefix listToAttrs literalExpression mapAttrsToList mdDoc optionals optionalAttrs optionalString types unique;
- sslOpts = { ... }: {
+ config' = config;
+ cfg = config.services.prosody;
- options = {
+ luaType = with types; let
+ valueType = nullOr
+ (oneOf [
+ bool
+ int
+ float
+ str
+ path
+ (attrsOf valueType)
+ (listOf valueType)
+ ]) // {
+ description = "Lua value";
+ };
+ in
+ valueType;
+
+ sslOption = mkOption {
+ type = with types; nullOr (submodule {
+ freeformType = luaType;
+ options = {
+
+ key = mkOption {
+ type = types.str;
+ description = mdDoc ''
+ Path to your private key file.
+ '';
+ };
- key = mkOption {
- type = types.path;
- description = "Path to the key file.";
- };
+ certificate = mkOption {
+ type = types.str;
+ description = mdDoc ''
+ Path to your certificate file.
+ '';
+ };
- # TODO: rename to certificate to match the prosody config
- cert = mkOption {
- type = types.path;
- description = "Path to the certificate file.";
- };
+ cafile = mkOption {
+ type = types.str;
+ default = "/etc/ssl/certs/ca-bundle.crt";
+ description = mdDoc ''
+ Path to a file containing root certificates that you wish Prosody to trust.
+ '';
+ };
- extraOptions = mkOption {
- type = types.attrs;
- default = {};
- description = "Extra SSL configuration options.";
};
+ });
+ default = null;
+ description = mdDoc ''
+ Advanced SSL/TLS configuration, as described by <https://prosody.im/doc/advanced_ssl_config>.
- };
- };
-
- discoOpts = {
- options = {
- url = mkOption {
- type = types.str;
- description = "URL of the endpoint you want to make discoverable";
- };
- description = mkOption {
- type = types.str;
- description = "A short description of the endpoint you want to advertise";
- };
- };
+ ::: {.note}
+ Prosody passes the contents of the `ssl` option from the config file almost directly to LuaSec, the
+ library used for SSL/TLS support in Prosody. LuaSec accepts a range of options here, mostly things
+ that it passes directly to OpenSSL. It is recommended to leave Prosody's defaults in most cases, unless
+ you know what you are doing.
+ You could easily reduce security or introduce unnecessary compatibility issues with clients and other servers!
+ :::
+ '';
};
- moduleOpts = {
- # Required for compliance with https://compliance.conversations.im/about/
- roster = mkOption {
- type = types.bool;
- default = true;
- description = "Allow users to have a roster";
- };
-
- saslauth = mkOption {
- type = types.bool;
- default = true;
- description = "Authentication for clients and servers. Recommended if you want to log in.";
- };
-
- tls = mkOption {
- type = types.bool;
- default = true;
- description = "Add support for secure TLS on c2s/s2s connections";
- };
-
- dialback = mkOption {
- type = types.bool;
- default = true;
- description = "s2s dialback support";
- };
-
- disco = mkOption {
- type = types.bool;
- default = true;
- description = "Service discovery";
- };
-
- # Not essential, but recommended
- carbons = mkOption {
- type = types.bool;
- default = true;
- description = "Keep multiple clients in sync";
- };
-
- csi = mkOption {
- type = types.bool;
- default = true;
- description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
- };
-
- cloud_notify = mkOption {
- type = types.bool;
- default = true;
- description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
- };
-
- pep = mkOption {
- type = types.bool;
- default = true;
- description = "Enables users to publish their mood, activity, playing music and more";
- };
-
- private = mkOption {
- type = types.bool;
- default = true;
- description = "Private XML storage (for room bookmarks, etc.)";
- };
-
- blocklist = mkOption {
- type = types.bool;
- default = true;
- description = "Allow users to block communications with other users";
- };
-
- vcard = mkOption {
- type = types.bool;
- default = false;
- description = "Allow users to set vCards";
- };
-
- vcard_legacy = mkOption {
- type = types.bool;
- default = true;
- description = "Converts users profiles and Avatars between old and new formats";
- };
-
- bookmarks = mkOption {
- type = types.bool;
- default = true;
- description = "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP";
- };
-
- # Nice to have
- version = mkOption {
- type = types.bool;
- default = true;
- description = "Replies to server version requests";
- };
-
- uptime = mkOption {
- type = types.bool;
- default = true;
- description = "Report how long server has been running";
- };
-
- time = mkOption {
- type = types.bool;
- default = true;
- description = "Let others know the time here on this server";
- };
+ componentsOption = mkOption {
+ type = with types; attrsOf (submodule ({ name, config, ... }: {
+ options = {
+ module = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = mdDoc ''
+ The name of the plugin you wish to use for the component if internal, or `null` if the
+ component is an external component.
+ '';
+ };
- ping = mkOption {
- type = types.bool;
- default = true;
- description = "Replies to XMPP pings with pongs";
- };
+ settings = mkOption {
+ type = luaType;
+ default = { };
+ description = mdDoc ''
+ Values specified here are applied to a specific component. Refer to <https://prosody.im/doc/components>
+ for additional details.
+ '';
+ };
- register = mkOption {
- type = types.bool;
- default = true;
- description = "Allow users to register on this server using a client and change passwords";
- };
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = mdDoc ''
+ Additional component specific configuration.
+ '';
+ };
- mam = mkOption {
- type = types.bool;
- default = true;
- description = "Store messages in an archive and allow users to access it";
- };
+ };
- smacks = mkOption {
- type = types.bool;
- default = true;
- description = "Allow a client to resume a disconnected session, and prevent message loss";
- };
+ config.settings = mkMerge [
+ (mkIf (config.module == "http_upload") {
+ http_upload_path = mkIf (config.module == "http_upload_path") cfg.dataDir;
+ })
+ (mkIf (config.module == "muc") {
+ modules_enabled = [ "muc_mam" ];
+ })
+ ];
+ }));
+ default = { };
+ description = mdDoc ''
+ Components are extra services on a server which are available to clients, usually on a subdomain
+ of the main server (such as mycomponent.example.com). Example components might be chatroom
+ servers, user directories, or gateways to other protocols.
+
+ See <https://prosody.im/doc/components> for details.
+ '';
+ };
- # Admin interfaces
- admin_adhoc = mkOption {
- type = types.bool;
- default = true;
- description = "Allows administration via an XMPP client that supports ad-hoc commands";
- };
+ acmeHosts = unique (mapAttrsToList (domain: hostOpts: hostOpts.useACMEHost) (filterAttrs (_: v: v.useACMEHost != null) cfg.virtualHosts));
+in
+{
+ imports = [
+ (mkRenamedOptionModule [ "services" "prosody" "allowRegistration" ] [ "services" "prosody" "settings" "allow_registration" ])
+ (mkRenamedOptionModule [ "services" "prosody" "httpPorts" ] [ "services" "prosody" "settings" "http_ports" ])
+ (mkRenamedOptionModule [ "services" "prosody" "httpInterfaces" ] [ "services" "prosody" "settings" "http_interfaces" ])
+ (mkRenamedOptionModule [ "services" "prosody" "httpsPorts" ] [ "services" "prosody" "settings" "https_ports" ])
+ (mkRenamedOptionModule [ "services" "prosody" "httpsInterfaces" ] [ "services" "prosody" "settings" "https_interfaces" ])
+ (mkRenamedOptionModule [ "services" "prosody" "c2sRequireEncryption" ] [ "services" "prosody" "settings" "c2s_require_encryption" ])
+ (mkRenamedOptionModule [ "services" "prosody" "s2sRequireEncryption" ] [ "services" "prosody" "settings" "s2s_require_encryption" ])
+ (mkRenamedOptionModule [ "services" "prosody" "s2sSecureAuth" ] [ "services" "prosody" "settings" "s2s_secure_auth" ])
+ (mkRenamedOptionModule [ "services" "prosody" "s2sInsecureDomains" ] [ "services" "prosody" "settings" "s2s_insecure_domains" ])
+ (mkRenamedOptionModule [ "services" "prosody" "s2sSecureDomains" ] [ "services" "prosody" "settings" "s2s_secure_domains" ])
+ (mkRenamedOptionModule [ "services" "prosody" "extraModules" ] [ "services" "prosody" "settings" "modules_enabled" ])
+ (mkRenamedOptionModule [ "services" "prosody" "extraPluginPaths" ] [ "services" "prosody" "settings" "plugin_paths" ])
+ (mkRenamedOptionModule [ "services" "prosody" "ssl" "key" ] [ "services" "prosody" "settings" "ssl" "key" ])
+ (mkRenamedOptionModule [ "services" "prosody" "ssl" "cert" ] [ "services" "prosody" "settings" "ssl" "certificate" ])
+ (mkRenamedOptionModule [ "services" "prosody" "ssl" "extraOptions" ] [ "services" "prosody" "settings" "ssl" ])
+ (mkRenamedOptionModule [ "services" "prosody" "admins" ] [ "services" "prosody" "settings" "admins" ])
+ (mkRenamedOptionModule [ "services" "prosody" "authentication" ] [ "services" "prosody" "settings" "authentication" ])
+ (mkChangedOptionModule [ "services" "prosody" "disco_items" ] [ "services" "prosody" "settings" "disco_items" ] (config:
+ map ({ url, description }: [ url description ]) config.services.prosody.disco_items
+ ))
+ (mkChangedOptionModule [ "services" "prosody" "uploadHttp" ] [ "services" "prosody" "components" ] (config:
+ let
+ cfg = config.services.prosody;
+ in
+ {
+ ${cfg.uploadHttp.domain} = {
+ module = "http_upload";
+ # these values are to preserve compatibility with this module pre nixos 23.11
+ settings = {
+ http_upload_file_size_limit = { __compat = true; value = cfg.uploadHttp.uploadFileSizeLimit or "50 * 1024 * 1024"; };
+ http_upload_expire_after = { __compat = true; value = cfg.uploadHttp.uploadExpireAfter or "60 * 60 * 24 * 7"; };
+ http_upload_path = cfg.uploadHttp.httpUploadPath or "/var/lib/prosody";
+ } // optionalAttrs (cfg.uploadHttp ? userQuota) {
+ http_upload_quota = cfg.uploadHttp.userQuota;
+ };
+ };
+ }
+ ))
+ (mkChangedOptionModule [ "services" "prosody" "muc" ] [ "services" "prosody" "components" ] (config:
+ listToAttrs (map
+ (muc: {
+ name = muc.domain;
+ value = {
+ module = "muc";
+ extraConfig = muc.extraConfig or "";
+ # these values are to preserve compatibility with this module pre nixos 23.11
+ settings = {
+ modules_enabled = optionals (muc.vcard_muc or true) [ "vcard_muc" ];
+ name = muc.name or "Prosody Chatrooms";
+ restrict_room_creation = muc.restrictRoomCreation or "local";
+ max_history_messages = muc.maxHistoryMessages or 20;
+ muc_room_locking = muc.roomLocking or true;
+ muc_room_lock_timeout = muc.roomLockTimeout or 300;
+ muc_tombstones = muc.tombstones or true;
+ muc_tombstone_expiry = muc.tombstoneExpiry or 2678400;
+ muc_room_default_public = muc.roomDefaultPublic or true;
+ muc_room_default_members_only = muc.roomDefaultMembersOnly or false;
+ muc_room_default_moderated = muc.roomDefaultModerated or false;
+ muc_room_default_public_jids = muc.roomDefaultPublicJids or false;
+ muc_room_default_change_subject = muc.roomDefaultChangeSubject or false;
+ muc_room_default_history_length = muc.roomDefaultHistoryLength or 20;
+ muc_room_default_language = muc.roomDefaultLanguage or "en";
+ };
+ };
+ })
+ config.services.prosody.muc)
+ ))
+ ];
- http_files = mkOption {
- type = types.bool;
- default = true;
- description = "Serve static files from a directory over HTTP";
- };
+ options.services.prosody = {
+ enable = mkEnableOption "Prosody, the modern XMPP communication server";
- proxy65 = mkOption {
+ xmppComplianceSuite = mkOption {
type = types.bool;
default = true;
- description = "Enables a file transfer proxy service which clients behind NAT can use";
- };
-
- admin_telnet = mkOption {
- type = types.bool;
- default = false;
- description = "Opens telnet console interface on localhost port 5582";
+ description = mdDoc ''
+ The XEP-0423 defines a set of recommended XEPs to implement
+ for a server. It's generally a good idea to implement this
+ set of extensions if you want to provide your users with a
+ good XMPP experience.
+
+ This NixOS module aims to provide an "advanced server"
+ experience as per defined in the [XEP-0423](https://xmpp.org/extensions/xep-0423.html) specification.
+
+ Setting this option to `true` will prevent you from building a
+ NixOS configuration which won't comply with this standard.
+ You can explicitly decide to ignore this standard if you
+ know what you are doing by setting this option to `false`.
+ '';
};
- # HTTP modules
- bosh = mkOption {
- type = types.bool;
- default = false;
- description = "Enable BOSH clients, aka 'Jabber over HTTP'";
+ package = mkOption {
+ type = types.package;
+ description = mdDoc "Prosody package to use.";
+ default = pkgs.prosody;
+ defaultText = literalExpression "pkgs.prosody";
+ example = literalExpression ''
+ pkgs.prosody.override {
+ withExtraLibs = [ pkgs.luaPackages.lpty ];
+ withCommunityModules = [ "auth_external" ];
+ };
+ '';
};
- websocket = mkOption {
- type = types.bool;
- default = false;
- description = "Enable WebSocket support";
- };
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/prosody";
+ description = mdDoc ''
+ The Prosody home directory used to store all data.
- # Other specific functionality
- limits = mkOption {
- type = types.bool;
- default = false;
- description = "Enable bandwidth limiting for XMPP connections";
+ ::: {.note}
+ If left as the default value this directory will automatically be created
+ before the Prosody server starts, otherwise you are responsible for
+ ensuring the directory exists with appropriate ownership and permissions.
+ :::
+ '';
};
- groups = mkOption {
- type = types.bool;
- default = false;
- description = "Shared roster support";
- };
+ user = mkOption {
+ type = types.str;
+ default = "prosody";
+ description = mdDoc ''
+ User account under which Prosody runs.
- server_contact_info = mkOption {
- type = types.bool;
- default = false;
- description = "Publish contact information for this service";
+ ::: {.note}
+ If left as the default value this user will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the user exists before the Prosody service starts.
+ :::
+ '';
};
- announce = mkOption {
- type = types.bool;
- default = false;
- description = "Send announcement to all online users";
- };
+ group = mkOption {
+ type = types.str;
+ default = "prosody";
+ description = mdDoc ''
+ Group account under which Prosody runs.
- welcome = mkOption {
- type = types.bool;
- default = false;
- description = "Welcome users who register accounts";
+ ::: {.note}
+ If left as the default value this group will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the group exists before the Prosody service starts.
+ :::
+ '';
};
- watchregistrations = mkOption {
- type = types.bool;
- default = false;
- description = "Alert admins of registrations";
- };
+ environmentFile = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ description = mdDoc ''
+ File which may contain secrets in the format of an EnvironmentFile as described
+ by systemd.exec(5). Variables here can be used in the Prosody configuration
+ file by prefixing the environment variable name with `ENV_` in the configuration.
- motd = mkOption {
- type = types.bool;
- default = false;
- description = "Send a message to users when they log in";
+ ::: {.note}
+ Environment variables in this file should *not* begin with an `ENV_` prefix, only
+ when referenced in the Prosody configuration file.
+ :::
+ '';
};
- legacyauth = mkOption {
+ openFirewall = mkOption {
type = types.bool;
default = false;
- description = "Legacy authentication. Only used by some old clients and bots";
- };
- };
+ description = mdDoc ''
+ Whether to automatically open ports specified by the following options:
- toLua = x:
- if builtins.isString x then ''"${x}"''
- else if builtins.isBool x then boolToString x
- else if builtins.isInt x then toString x
- else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
- else throw "Invalid Lua value";
-
- settingsToLua = prefix: settings: generators.toKeyValue {
- listsAsDuplicateKeys = false;
- mkKeyValue = k:
- generators.mkKeyValueDefault {
- mkValueString = toLua;
- } " = " (prefix + k);
- }
- (filterAttrs (k: v: v != null) settings);
-
- createSSLOptsStr = o: ''
- ssl = {
- cafile = "/etc/ssl/certs/ca-bundle.crt";
- key = "${o.key}";
- certificate = "${o.cert}";
- ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
- };
- '';
-
- mucOpts = { ... }: {
- options = {
- domain = mkOption {
- type = types.str;
- description = "Domain name of the MUC";
- };
- name = mkOption {
- type = types.str;
- description = "The name to return in service discovery responses for the MUC service itself";
- default = "Prosody Chatrooms";
- };
- restrictRoomCreation = mkOption {
- type = types.enum [ true false "admin" "local" ];
- default = false;
- description = "Restrict room creation to server admins";
- };
- maxHistoryMessages = mkOption {
- type = types.int;
- default = 20;
- description = "Specifies a limit on what each room can be configured to keep";
- };
- roomLocking = mkOption {
- type = types.bool;
- default = true;
- description = ''
- Enables room locking, which means that a room must be
- configured before it can be used. Locked rooms are invisible
- and cannot be entered by anyone but the creator
- '';
- };
- roomLockTimeout = mkOption {
- type = types.int;
- default = 300;
- description = ''
- Timout after which the room is destroyed or unlocked if not
- configured, in seconds
- '';
- };
- tombstones = mkOption {
- type = types.bool;
- default = true;
- description = ''
- When a room is destroyed, it leaves behind a tombstone which
- prevents the room being entered or recreated. It also allows
- anyone who was not in the room at the time it was destroyed
- to learn about it, and to update their bookmarks. Tombstones
- prevents the case where someone could recreate a previously
- semi-anonymous room in order to learn the real JIDs of those
- who often join there.
- '';
- };
- tombstoneExpiry = mkOption {
- type = types.int;
- default = 2678400;
- description = ''
- This settings controls how long a tombstone is considered
- valid. It defaults to 31 days. After this time, the room in
- question can be created again.
- '';
- };
-
- vcard_muc = mkOption {
- type = types.bool;
- default = true;
- description = "Adds the ability to set vCard for Multi User Chat rooms";
- };
-
- # Extra parameters. Defaulting to prosody default values.
- # Adding them explicitly to make them visible from the options
- # documentation.
- #
- # See https://prosody.im/doc/modules/mod_muc for more details.
- roomDefaultPublic = mkOption {
- type = types.bool;
- default = true;
- description = "If set, the MUC rooms will be public by default.";
- };
- roomDefaultMembersOnly = mkOption {
- type = types.bool;
- default = false;
- description = "If set, the MUC rooms will only be accessible to the members by default.";
- };
- roomDefaultModerated = mkOption {
- type = types.bool;
- default = false;
- description = "If set, the MUC rooms will be moderated by default.";
- };
- roomDefaultPublicJids = mkOption {
- type = types.bool;
- default = false;
- description = "If set, the MUC rooms will display the public JIDs by default.";
- };
- roomDefaultChangeSubject = mkOption {
- type = types.bool;
- default = false;
- description = "If set, the rooms will display the public JIDs by default.";
- };
- roomDefaultHistoryLength = mkOption {
- type = types.int;
- default = 20;
- description = "Number of history message sent to participants by default.";
- };
- roomDefaultLanguage = mkOption {
- type = types.str;
- default = "en";
- description = "Default room language.";
- };
- extraConfig = mkOption {
- type = types.lines;
- default = "";
- description = "Additional MUC specific configuration";
- };
- };
- };
-
- uploadHttpOpts = { ... }: {
- options = {
- domain = mkOption {
- type = types.nullOr types.str;
- description = "Domain name for the http-upload service";
- };
- uploadFileSizeLimit = mkOption {
- type = types.str;
- default = "50 * 1024 * 1024";
- description = "Maximum file size, in bytes. Defaults to 50MB.";
- };
- uploadExpireAfter = mkOption {
- type = types.str;
- default = "60 * 60 * 24 * 7";
- description = "Max age of a file before it gets deleted, in seconds.";
- };
- userQuota = mkOption {
- type = types.nullOr types.int;
- default = null;
- example = 1234;
- description = ''
- Maximum size of all uploaded files per user, in bytes. There
- will be no quota if this option is set to null.
- '';
- };
- httpUploadPath = mkOption {
- type = types.str;
- description = ''
- Directory where the uploaded files will be stored
- when the http_upload module is used.
- By default, uploaded files are put in a sub-directory of the
- default Prosody storage path (usually /var/lib/prosody).
- '';
- default = "/var/lib/prosody";
- };
- };
- };
-
- httpFileShareOpts = { ... }: {
- freeformType = with types;
- let atom = oneOf [ int bool str (listOf atom) ]; in
- attrsOf (nullOr atom);
- options.domain = mkOption {
- type = with types; nullOr str;
- description = "Domain name for a http_file_share service.";
+ - {option}`services.prosody.settings.c2s_ports`
+ - {option}`services.prosody.settings.s2s_ports`
+ - {option}`services.prosody.settings.https_ports`
+ '';
};
- };
-
- vHostOpts = { ... }: {
- options = {
+ settings = mkOption {
+ type = types.submodule {
+ freeformType = luaType;
+ options = {
- # TODO: require attribute
- domain = mkOption {
- type = types.str;
- description = "Domain name";
- };
+ modules_enabled = mkOption {
+ type = with types; listOf str;
+ apply = x: unique x;
+ default = [ ];
+ description = mdDoc ''
+ List of modules to load for all virtual hosts.
+ '';
+ };
- enabled = mkOption {
- type = types.bool;
- default = false;
- description = "Whether to enable the virtual host";
- };
+ modules_disabled = mkOption {
+ type = with types; listOf str;
+ apply = x: unique x;
+ default = [ ];
+ description = mdDoc ''
+ Allows you to disable the loading of a list of modules for all virtual hosts
+ if those modules are set in the global settings.
+ '';
+ };
- ssl = mkOption {
- type = types.nullOr (types.submodule sslOpts);
- default = null;
- description = "Paths to SSL files";
- };
+ ssl = sslOption;
- extraConfig = mkOption {
- type = types.lines;
- default = "";
- description = "Additional virtual host specific configuration";
+ };
};
+ default = { };
+ description = mdDoc ''
+ Values specified here are applied to the whole server, and are the default for all
+ virtual hosts. Refer to <https://prosody.im/doc/configure> for additional details.
+ ::: {.note}
+ It's also possible to refer to environment variables (defined in
+ [services.prosody.environmentFile](#opt-services.prosody.environmentFile)) using the
+ syntax `"ENV_VARIABLE_NAME"` where the environment file contains a `VARIABLE_NAME` entry.
+ :::
+ '';
+ example = literalExpression ''
+ {
+ modules_enabled = [ "turn_external" ];
+ turn_external_host = "turn.example.com";
+ turn_external_port = 3478;
+ turn_external_secret = "ENV_TURN_EXTERNAL_SECRET";
+ }
+ '';
};
- };
-
-in
-
-{
-
- ###### interface
-
- options = {
-
- services.prosody = {
+ components = componentsOption;
- enable = mkOption {
- type = types.bool;
- default = false;
- description = "Whether to enable the prosody server";
- };
-
- xmppComplianceSuite = mkOption {
- type = types.bool;
- default = true;
- description = ''
- The XEP-0423 defines a set of recommended XEPs to implement
- for a server. It's generally a good idea to implement this
- set of extensions if you want to provide your users with a
- good XMPP experience.
-
- This NixOS module aims to provide a "advanced server"
- experience as per defined in the XEP-0423[1] specification.
-
- Setting this option to true will prevent you from building a
- NixOS configuration which won't comply with this standard.
- You can explicitely decide to ignore this standard if you
- know what you are doing by setting this option to false.
-
- [1] https://xmpp.org/extensions/xep-0423.html
- '';
- };
-
- package = mkOption {
- type = types.package;
- description = "Prosody package to use";
- default = pkgs.prosody;
- defaultText = literalExpression "pkgs.prosody";
- example = literalExpression ''
- pkgs.prosody.override {
- withExtraLibs = [ pkgs.luaPackages.lpty ];
- withCommunityModules = [ "auth_external" ];
+ virtualHosts = let config' = config; in mkOption {
+ type = with types; attrsOf (submodule ({ name, config, ... }: {
+ options = {
+ domain = mkOption {
+ type = types.str;
+ default = name;
+ description = mdDoc "Domain name.";
};
- '';
- };
-
- dataDir = mkOption {
- type = types.path;
- description = "Directory where Prosody stores its data";
- default = "/var/lib/prosody";
- };
-
- disco_items = mkOption {
- type = types.listOf (types.submodule discoOpts);
- default = [];
- description = "List of discoverable items you want to advertise.";
- };
-
- user = mkOption {
- type = types.str;
- default = "prosody";
- description = "User account under which prosody runs.";
- };
-
- group = mkOption {
- type = types.str;
- default = "prosody";
- description = "Group account under which prosody runs.";
- };
-
- allowRegistration = mkOption {
- type = types.bool;
- default = false;
- description = "Allow account creation";
- };
- # HTTP server-related options
- httpPorts = mkOption {
- type = types.listOf types.int;
- description = "Listening HTTP ports list for this service.";
- default = [ 5280 ];
- };
-
- httpInterfaces = mkOption {
- type = types.listOf types.str;
- default = [ "*" "::" ];
- description = "Interfaces on which the HTTP server will listen on.";
- };
-
- httpsPorts = mkOption {
- type = types.listOf types.int;
- description = "Listening HTTPS ports list for this service.";
- default = [ 5281 ];
- };
-
- httpsInterfaces = mkOption {
- type = types.listOf types.str;
- default = [ "*" "::" ];
- description = "Interfaces on which the HTTPS server will listen on.";
- };
-
- c2sRequireEncryption = mkOption {
- type = types.bool;
- default = true;
- description = ''
- Force clients to use encrypted connections? This option will
- prevent clients from authenticating unless they are using encryption.
- '';
- };
-
- s2sRequireEncryption = mkOption {
- type = types.bool;
- default = true;
- description = ''
- Force servers to use encrypted connections? This option will
- prevent servers from authenticating unless they are using encryption.
- Note that this is different from authentication.
- '';
- };
-
- s2sSecureAuth = mkOption {
- type = types.bool;
- default = false;
- description = ''
- Force certificate authentication for server-to-server connections?
- This provides ideal security, but requires servers you communicate
- with to support encryption AND present valid, trusted certificates.
- For more information see https://prosody.im/doc/s2s#security
- '';
- };
-
- s2sInsecureDomains = mkOption {
- type = types.listOf types.str;
- default = [];
- example = [ "insecure.example.com" ];
- description = ''
- Some servers have invalid or self-signed certificates. You can list
- remote domains here that will not be required to authenticate using
- certificates. They will be authenticated using DNS instead, even
- when s2s_secure_auth is enabled.
- '';
- };
+ useACMEHost = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = mdDoc ''
+ A host of an existing Let's Encrypt certificate to use.
+
+ ::: {.note}
+ Note that this option does not create any certificates, nor it does add subdomains to existing
+ ones – you will need to create them manually using [](#opt-security.acme.certs).
+ :::
+ '';
+ };
- s2sSecureDomains = mkOption {
- type = types.listOf types.str;
- default = [];
- example = [ "jabber.org" ];
- description = ''
- Even if you leave s2s_secure_auth disabled, you can still require valid
- certificates for some domains by specifying a list here.
- '';
- };
+ components = componentsOption;
+
+ settings = mkOption {
+ type = types.submodule {
+ freeformType = luaType;
+ options = {
+
+ enabled = mkOption {
+ type = types.bool;
+ default = true;
+ description = mdDoc ''
+ Specifies whether this host is enabled or not. Disabled hosts are not loaded and
+ do not accept connections while Prosody is running.
+ '';
+ };
+
+ modules_enabled = mkOption {
+ type = with types; listOf str;
+ apply = x: unique x;
+ default = [ ];
+ description = mdDoc ''
+ List of modules to load for the virtual host.
+ '';
+ };
+
+ modules_disabled = mkOption {
+ type = with types; listOf str;
+ apply = x: unique x;
+ default = [ ];
+ description = mdDoc ''
+ Allows you to disable the loading of a list of modules for a particular host.
+ '';
+ };
+
+ ssl = sslOption;
+ };
+ };
+ default = { };
+ description = mdDoc ''
+ Values specified here are applied to a specific virtual host and will override values set
+ in the global [settings](#opt-services.prosody.settings) option. Refer to <https://prosody.im/doc/configure>
+ for additional details.
+ '';
+ };
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = mdDoc ''
+ Additional virtual host specific configuration.
+ '';
+ };
- modules = moduleOpts;
+ # options to preserve compatibility with this module pre nixos 23.11
- extraModules = mkOption {
- type = types.listOf types.str;
- default = [];
- description = "Enable custom modules";
- };
+ enabled = mkOption {
+ type = with types; nullOr bool;
+ default = null;
+ description = mdDoc "Whether to enable the virtual host.";
+ };
- extraPluginPaths = mkOption {
- type = types.listOf types.path;
- default = [];
- description = "Addtional path in which to look find plugins/modules";
- };
+ ssl = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ key = mkOption {
+ type = types.path;
+ description = lib.mdDoc "Path to the key file.";
+ };
+
+ cert = mkOption {
+ type = types.path;
+ description = lib.mdDoc "Path to the certificate file.";
+ };
+
+ extraOptions = mkOption {
+ type = types.attrs;
+ default = { };
+ description = lib.mdDoc "Extra SSL configuration options.";
+ };
+ };
+ });
+ default = null;
+ description = mdDoc "Paths to SSL files.";
+ };
- uploadHttp = mkOption {
- description = ''
- Configures the old Prosody builtin HTTP server to handle user uploads.
- '';
- type = types.nullOr (types.submodule uploadHttpOpts);
- default = null;
- example = {
- domain = "uploads.my-xmpp-example-host.org";
};
- };
- httpFileShare = mkOption {
- description = ''
- Configures the http_file_share module to handle user uploads.
- '';
- type = types.nullOr (types.submodule httpFileShareOpts);
- default = null;
- example = {
- domain = "uploads.my-xmpp-example-host.org";
+ config.settings = {
+ ssl = mkMerge [
+ (mkIf (config.ssl != null) ({
+ key = config.ssl.key;
+ certificate = config.ssl.cert;
+ } // config.ssl.extraOptions))
+ (mkIf (config.useACMEHost != null) {
+ key = "${config'.security.acme.certs.${config.useACMEHost}.directory}/key.pem";
+ certificate = "${config'.security.acme.certs.${config.useACMEHost}.directory}/fullchain.pem";
+ })
+ ];
+
+ disco_items =
+ mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") config.components) ++
+ mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") config.components)
+ ;
+
+ enabled = mkIf (config.enabled != null) config.enabled;
};
+ }));
+ default = {
+ localhost = { };
};
-
- muc = mkOption {
- type = types.listOf (types.submodule mucOpts);
- default = [ ];
- example = [ {
- domain = "conference.my-xmpp-example-host.org";
- } ];
- description = "Multi User Chat (MUC) configuration";
- };
-
- virtualHosts = mkOption {
-
- description = "Define the virtual hosts";
-
- type = with types; attrsOf (submodule vHostOpts);
-
- example = {
- myhost = {
- domain = "my-xmpp-example-host.org";
- enabled = true;
- };
- };
-
- default = {
- localhost = {
- domain = "localhost";
- enabled = true;
+ description = mdDoc ''
+ A host in Prosody is a domain on which user accounts can be created. For example if you want your users to have addresses
+ like `john.smith@example.com` then you need to add a host `example.com`.
+ '';
+ example = literalExpression ''
+ {
+ "example.net" = {
+ useACMEHost = "example.net";
+ settings = {
+ admins = [ "admin1@example.net" "admin2@example.net" ];
+ c2s_require_encryption = true;
+ modules_enabled = [
+ "announce"
+ ];
+ };
};
- };
-
- };
-
- ssl = mkOption {
- type = types.nullOr (types.submodule sslOpts);
- default = null;
- description = "Paths to SSL files";
- };
-
- admins = mkOption {
- type = types.listOf types.str;
- default = [];
- example = [ "admin1@example.com" "admin2@example.com" ];
- description = "List of administrators of the current host";
- };
+ }
+ '';
+ };
- authentication = mkOption {
- type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
- default = "internal_hashed";
- example = "internal_plain";
- description = "Authentication mechanism used for logins.";
- };
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = mdDoc ''
+ Additional prosody configuration.
+ '';
+ };
- extraConfig = mkOption {
- type = types.lines;
- default = "";
- description = "Additional prosody configuration";
- };
+ # options to preserve compatibility with this module pre nixos 23.11
+ modules = mkOption {
+ type = types.attrsOf types.bool;
+ default = { };
+ description = mdDoc ''
+ '';
};
};
-
- ###### implementation
-
config = mkIf cfg.enable {
- assertions = let
- genericErrMsg = ''
+ assertions =
+ let
+ hasAnyModules = mods: b: any (e: elem e.module mods) (attrValues b.components);
+ virtualHosts = filterAttrs (_: v: v.settings.enabled) cfg.virtualHosts;
- Having a server not XEP-0423-compliant might make your XMPP
- experience terrible. See the NixOS manual for further
- informations.
+ # ensure a given module (e.g. muc or http_upload) is applied or available to every virtual host
+ mkAssertion = modules: message: {
+ assertion = cfg.xmppComplianceSuite -> hasAnyModules modules cfg || all (hasAnyModules modules) (attrValues virtualHosts);
+ message = message + ''
- If you know what you're doing, you can disable this warning by
- setting config.services.prosody.xmppComplianceSuite to false.
- '';
- errors = [
- { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
- message = ''
- You need to setup at least a MUC domain to comply with
- XEP-0423.
- '' + genericErrMsg;}
- { assertion = cfg.uploadHttp != null || cfg.httpFileShare != null || !cfg.xmppComplianceSuite;
- message = ''
- You need to setup the http_upload or http_file_share modules through
- config.services.prosody.uploadHttp
- or config.services.prosody.httpFileShare
- to comply with XEP-0423.
- '' + genericErrMsg;}
+ Having a server not XEP-0423-compliant might make your XMPP
+ experience terrible. See the NixOS manual for further
+ information.
+
+ If you know what you're doing, you can disable this warning by
+ setting config.services.prosody.xmppComplianceSuite to false.
+ '';
+ };
+ in
+ [
+ (mkAssertion [ "muc" ] ''
+ You need to setup at least a MUC domain to comply with
+ XEP-0423
+ '')
+
+ (mkAssertion [ "http_upload" "http_file_share" ] ''
+ You need to setup the http_upload or http_file_share module through
+ config.services.prosody.components to comply with
+ XEP-0423.
+ '')
];
- in errors;
- environment.systemPackages = [ cfg.package ];
+ warnings =
+ 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'." ] ++
+ 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) ++
+ 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)
+ ;
+
+ services.prosody.settings = {
+ log = mkDefault "*syslog";
+ data_path = mkForce cfg.dataDir;
+ network_backend = "event";
+ pidfile = "/run/prosody/prosody.pid";
+
+ authentication = mkDefault "internal_hashed";
+ reload_modules = mkIf (acmeHosts != [ ]) [ "tls" ];
+
+ modules_enabled = [
+ # required for compliance with https://compliance.conversations.im/about/
+ "dialback"
+ "disco"
+ "roster"
+ "saslauth"
+ "tls"
+
+ # not essential, but recommended
+ "blocklist"
+ "bookmarks"
+ "carbons"
+ "cloud_notify"
+ "csi"
+ "pep"
+ "private"
+ "vcard_legacy"
+
+ # nice to have
+ "mam"
+ "ping"
+ "register"
+ "smacks"
+ "time"
+ "uptime"
+ "version"
+
+ # admin interfaces
+ "admin_adhoc"
+ "http_files"
+ "proxy65"
+ ] ++ cfg.package.communityModules
+ ++ mapAttrsToList (k: _: k) (filterAttrs (_: v: v == true) cfg.modules);
+
+ modules_disabled = mapAttrsToList (k: _: k) (filterAttrs (_: v: v == false) cfg.modules);
+
+ disco_items =
+ mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") cfg.components) ++
+ mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") cfg.components)
+ ;
+
+ # mod_tls configuration
+ c2s_require_encryption = mkDefault true;
+ s2s_require_encryption = mkDefault true;
+
+ # upstream defaults - useful for `services.prosody.openFirewall` logic
+ c2s_ports = mkDefault [ 5222 ];
+ s2s_ports = mkDefault [ 5269 ];
+ https_ports = mkDefault [ 5281 ];
+ # TODO:
+ # proxy65_ports = [ 5000 ];
+ };
+ environment.systemPackages = [ cfg.package ];
environment.etc."prosody/prosody.cfg.lua".text =
let
- httpDiscoItems =
- optional (cfg.uploadHttp != null)
- { url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";} ++
- optional (cfg.httpFileShare != null)
- { url = cfg.httpFileShare.domain; description = "HTTP file share endpoint";};
- mucDiscoItems = builtins.foldl'
- (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
- []
- cfg.muc;
- discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
- in ''
-
- pidfile = "/run/prosody/prosody.pid"
-
- log = "*syslog"
-
- data_path = "${cfg.dataDir}"
- plugin_paths = {
- ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
- }
-
- ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
-
- admins = ${toLua cfg.admins}
-
- -- we already build with libevent, so we can just enable it for a more performant server
- use_libevent = true
-
- modules_enabled = {
-
- ${ lib.concatStringsSep "\n " (lib.mapAttrsToList
- (name: val: optionalString val "${toLua name};")
- cfg.modules) }
- ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
- ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
- };
-
- disco_items = {
- ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
- };
-
- allow_registration = ${toLua cfg.allowRegistration}
-
- c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
-
- s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
-
- s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
-
- s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
-
- s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
-
- authentication = ${toLua cfg.authentication}
-
- http_interfaces = ${toLua cfg.httpInterfaces}
-
- https_interfaces = ${toLua cfg.httpsInterfaces}
-
- http_ports = ${toLua cfg.httpPorts}
-
- https_ports = ${toLua cfg.httpsPorts}
-
- ${ cfg.extraConfig }
+ toFormat = attrs: concatStringsSep "\n" (mapAttrsToList (k: v: ''${k} = ${toStr v}'') (filterAttrs (_: v: v != null) attrs));
+
+ toStr = v:
+ if isString v then
+ # prosody will directly pass environment variables into its configuration file which have `ENV_` as a prefix
+ if hasPrefix "ENV_" v then v else ''"${toString v}"''
+ else if isBool v then boolToString v
+ else if isInt v then toString v
+ else if isList v then ''{ ${concatStringsSep ", " (map (n: toStr n) v)} }''
+ else if isAttrs v then
+ if v ? __compat then v.value else ''{ ${concatStringsSep ", " (mapAttrsToList (a: b: ''["${a}"] = ${toStr b}'') v)} }''
+ else throw "Invalid Lua value";
+
+ componentsToStr = components:
+ concatStringsSep "\n" (
+ mapAttrsToList
+ (domain: component:
+ ''
+ Component "${domain}" ${optionalString (component.module != null) "\"${component.module}\""}
+ ${toFormat component.settings}
+ ${component.extraConfig}
+ ''
+ )
+ components
+ );
+
+ virtualHostsToStr = virtualHosts:
+ concatStringsSep "\n" (
+ mapAttrsToList
+ (_: virtualHost:
+ ''
+ VirtualHost "${virtualHost.domain}"
+ ${toFormat virtualHost.settings}
+ ${virtualHost.extraConfig}
+
+ ${componentsToStr virtualHost.components}
+ ''
+ )
+ virtualHosts
+ );
+ in
+ ''
+ ${toFormat cfg.settings}
+ ${cfg.extraConfig}
+
+ ${componentsToStr cfg.components}
+ ${virtualHostsToStr cfg.virtualHosts}
+ '';
- ${lib.concatMapStrings (muc: ''
- Component ${toLua muc.domain} "muc"
- modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
- name = ${toLua muc.name}
- restrict_room_creation = ${toLua muc.restrictRoomCreation}
- max_history_messages = ${toLua muc.maxHistoryMessages}
- muc_room_locking = ${toLua muc.roomLocking}
- muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
- muc_tombstones = ${toLua muc.tombstones}
- muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
- muc_room_default_public = ${toLua muc.roomDefaultPublic}
- muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
- muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
- muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
- muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
- muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
- muc_room_default_language = ${toLua muc.roomDefaultLanguage}
- ${ muc.extraConfig }
- '') cfg.muc}
+ systemd.services.prosody = {
+ description = "Prosody XMPP server";
+ wantedBy = [ "multi-user.target" ];
+ before = map (domain: "acme-${domain}.service") acmeHosts;
+ after = [ "network-online.target" ] ++ map (domain: "acme-selfsigned-${domain}.service") acmeHosts;
+ wants = [ "network-online.target" ] ++ map (domain: "acme-finished-${domain}.target") acmeHosts;
- ${ lib.optionalString (cfg.uploadHttp != null) ''
- Component ${toLua cfg.uploadHttp.domain} "http_upload"
- http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
- http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
- ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
- http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
- ''}
+ restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
+ serviceConfig = mkMerge [
+ {
+ User = cfg.user;
+ Group = cfg.group;
+ Type = "forking";
+ PIDFile = cfg.settings.pidfile;
+ ExecStart = "${cfg.package}/bin/prosodyctl start";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+ EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+
+ MemoryDenyWriteExecute = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateTmp = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ }
+ (mkIf (cfg.dataDir == "/var/lib/prosody") {
+ StateDirectory = "prosody";
+ StateDirectoryMode = "0750";
+ })
+ (mkIf (cfg.settings.pidfile == "/run/prosody/prosody.pid") {
+ RuntimeDirectory = [ "prosody" ];
+ })
+ ];
+ };
- ${ lib.optionalString (cfg.httpFileShare != null) ''
- Component ${toLua cfg.httpFileShare.domain} "http_file_share"
- ${settingsToLua " http_file_share_" (cfg.httpFileShare // { domain = null; })}
- ''}
+ security.acme.certs = genAttrs acmeHosts (_: {
+ reloadServices = [ "prosody.service" ];
+ });
- ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
- VirtualHost "${v.domain}"
- enabled = ${boolToString v.enabled};
- ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
- ${ v.extraConfig }
- '') cfg.virtualHosts) }
- '';
+ networking.firewall = optionalAttrs cfg.openFirewall {
+ allowedTCPPorts = cfg.settings.c2s_ports
+ ++ cfg.settings.s2s_ports
+ ++ cfg.settings.https_ports
+ # TODO: cfg.settings.proxy65_ports
+ ;
+ };
users.users.prosody = mkIf (cfg.user == "prosody") {
uid = config.ids.uids.prosody;
description = "Prosody user";
- createHome = true;
inherit (cfg) group;
- home = "${cfg.dataDir}";
+ home = cfg.dataDir;
};
users.groups.prosody = mkIf (cfg.group == "prosody") {
gid = config.ids.gids.prosody;
};
-
- systemd.services.prosody = {
- description = "Prosody XMPP server";
- after = [ "network-online.target" ];
- wants = [ "network-online.target" ];
- wantedBy = [ "multi-user.target" ];
- restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
- serviceConfig = {
- User = cfg.user;
- Group = cfg.group;
- Type = "forking";
- RuntimeDirectory = [ "prosody" ];
- PIDFile = "/run/prosody/prosody.pid";
- ExecStart = "${cfg.package}/bin/prosodyctl start";
- ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-
- MemoryDenyWriteExecute = true;
- PrivateDevices = true;
- PrivateMounts = true;
- PrivateTmp = true;
- ProtectControlGroups = true;
- ProtectHome = true;
- ProtectHostname = true;
- ProtectKernelModules = true;
- ProtectKernelTunables = true;
- RestrictNamespaces = true;
- RestrictRealtime = true;
- RestrictSUIDSGID = true;
- };
- };
-
};
- meta.doc = ./prosody.xml;
+
+ meta.doc = ./prosody.md;
}