8 cfg = config.services.mosquitto;
10 # note that mosquitto config parsing is very simplistic as of may 2021.
11 # often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
12 # there's no escaping available either, so we have to prevent any being necessary.
13 str = lib.types.strMatching "[^\r\n]*" // {
14 description = "single-line string";
16 path = lib.types.addCheck lib.types.path (p: str.check "${p}");
17 configKey = lib.types.strMatching "[^\r\n\t ]+";
27 description = "string, path, bool, or integer";
33 else if path.check v then
39 prefix: valid: config:
40 lib.mapAttrsToList (n: _: {
41 assertion = valid ? ${n};
42 message = "Invalid config key ${prefix}.${n}.";
49 lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
55 password = lib.mkOption {
56 type = uniq (nullOr str);
59 Specifies the (clear text) password for the MQTT User.
63 passwordFile = lib.mkOption {
64 type = uniq (nullOr path);
65 example = "/path/to/file";
68 Specifies the path to a file containing the
69 clear text password for the MQTT user.
70 The file is securely passed to mosquitto by
71 leveraging systemd credentials. No special
72 permissions need to be set on this file.
76 hashedPassword = lib.mkOption {
77 type = uniq (nullOr str);
80 Specifies the hashed password for the MQTT User.
81 To generate hashed password install the `mosquitto`
82 package and use `mosquitto_passwd`, then extract
83 the second field (after the `:`) from the generated
88 hashedPasswordFile = lib.mkOption {
89 type = uniq (nullOr path);
90 example = "/path/to/file";
93 Specifies the path to a file containing the
94 hashed password for the MQTT user.
95 To generate hashed password install the `mosquitto`
96 package and use `mosquitto_passwd`, then remove the
97 `username:` prefix from the generated file.
98 The file is securely passed to mosquitto by
99 leveraging systemd credentials. No special
100 permissions need to be set on this file.
112 Control client access to topics on the broker.
120 lib.mapAttrsToList (n: _: {
121 assertion = builtins.match "[^:\r\n]+" n != null;
122 message = "Invalid user name ${n} in ${prefix}";
124 ++ lib.mapAttrsToList (n: u: {
126 lib.count (s: s != null) [
132 message = "Cannot set more than one password option for user ${n} in ${prefix}";
135 listenerScope = index: "listener-${toString index}";
136 userScope = prefix: index: "${prefix}-user-${toString index}";
137 credentialID = prefix: credential: "${prefix}-${credential}";
140 listenerScope: users:
144 index: user: lib.nameValuePair user (users.${user} // { scope = userScope listenerScope index; })
151 lib.pipe credentials [
152 (lib.filter (credential: user.${credential} != null))
153 (map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
156 listenerScope: users: credentials:
158 (toScopedUsers listenerScope)
159 (lib.mapAttrsToList (_: user: userCredentials user credentials))
163 listeners: listenerCredentials:
165 (lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
170 listenerScope: users: path:
175 scopedUsers = toScopedUsers listenerScope users;
179 ''addLine ${lib.escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"''
180 ) (lib.filterAttrs (_: user: user.${store} != null) scopedUsers)
181 ++ lib.mapAttrsToList (
183 ''addFile ${lib.escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"''
184 ) (lib.filterAttrs (_: user: user.${file} != null) scopedUsers);
185 plainLines = makeLines "password" "passwordFile";
186 hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
188 pkgs.writeScript "make-mosquitto-passwd" (
190 #! ${pkgs.runtimeShell}
194 file=${lib.escapeShellArg path}
200 echo "$1:$2" >> "$file"
203 if [ $(wc -l <"$2") -gt 1 ]; then
204 echo "invalid mosquitto password file $2" >&2
207 echo "$1:$(cat "$2")" >> "$file"
210 + lib.concatStringsSep "\n" (
212 ++ lib.optional (plainLines != [ ]) ''
213 ${cfg.package}/bin/mosquitto_passwd -U "$file"
223 plugin = lib.mkOption {
226 Plugin path to load, should be a `.so` file.
230 denySpecialChars = lib.mkOption {
233 Automatically disallow all clients using `#`
234 or `+` in their name/id.
239 options = lib.mkOption {
240 type = attrsOf optionType;
242 Options for the auth plugin. Each key turns into a `auth_opt_*`
252 lib.mapAttrsToList (n: _: {
253 assertion = configKey.check n;
254 message = "Invalid auth plugin key ${prefix}.${n}";
260 "auth_plugin ${plugin.plugin}"
261 "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
263 ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
265 freeformListenerKeys = {
267 allow_zero_length_clientid = 1;
274 "ciphers_tls1.3" = 1;
286 require_certificate = 1;
289 tls_engine_kpass_sha1 = 1;
292 use_identity_as_username = 1;
293 use_subject_as_username = 1;
294 use_username_as_clientid = 1;
301 port = lib.mkOption {
304 Port to listen on. Must be set to 0 to listen on a unix domain socket.
309 address = lib.mkOption {
312 Address to listen on. Listen on `0.0.0.0`/`::`
318 authPlugins = lib.mkOption {
319 type = listOf authPluginOptions;
321 Authentication plugin to attach to this listener.
322 Refer to the [mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html)
323 for details on authentication plugins.
328 users = lib.mkOption {
329 type = attrsOf userOptions;
333 acl = [ "readwrite john/#" ];
337 A set of users and their passwords and ACLs.
342 omitPasswordAuth = lib.mkOption {
345 Omits password checking, allowing anyone to log in with any user name unless
346 other mandatory authentication methods (eg TLS client certificates) are configured.
354 Additional ACL items to prepend to the generated ACL file.
358 "topic readwrite anon/report/#"
363 settings = lib.mkOption {
365 freeformType = attrsOf optionType;
368 Additional settings for this listener.
377 assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
378 ++ userAsserts prefix listener.users
379 ++ lib.imap0 (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v) listener.authPlugins;
384 "listener ${toString listener.port} ${toString listener.address}"
385 "acl_file /etc/mosquitto/acl-${toString idx}.conf"
387 ++ lib.optional (!listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
388 ++ formatFreeform { } listener.settings
389 ++ lib.concatMap formatAuthPlugin listener.authPlugins;
391 freeformBridgeKeys = {
393 bridge_attempt_unsubscribe = 1;
394 bridge_bind_address = 1;
401 bridge_max_packet_size = 1;
402 bridge_outgoing_retain = 1;
403 bridge_protocol_version = 1;
405 bridge_require_ocsp = 1;
406 bridge_tls_version = 1;
409 keepalive_interval = 1;
410 local_cleansession = 1;
414 notification_topic = 1;
416 notifications_local_only = 1;
431 addresses = lib.mkOption {
432 type = listOf (submodule {
434 address = lib.mkOption {
437 Address of the remote MQTT broker.
441 port = lib.mkOption {
444 Port of the remote MQTT broker.
452 Remote endpoints for the bridge.
456 topics = lib.mkOption {
459 Topic patterns to be shared between the two brokers.
461 mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
464 example = [ "# both 2 local/topic/ remote/topic/" ];
467 settings = lib.mkOption {
469 freeformType = attrsOf optionType;
472 Additional settings for this bridge.
481 assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
484 assertion = lib.length bridge.addresses > 0;
485 message = "Bridge ${prefix} needs remote broker addresses";
493 "addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
495 ++ map (t: "topic ${t}") bridge.topics
496 ++ formatFreeform { } bridge.settings;
498 freeformGlobalKeys = {
499 allow_duplicate_messages = 1;
500 autosave_interval = 1;
501 autosave_on_changes = 1;
502 check_retain_source = 1;
503 connection_messages = 1;
506 log_timestamp_format = 1;
507 max_inflight_bytes = 1;
508 max_inflight_messages = 1;
511 max_queued_bytes = 1;
512 max_queued_messages = 1;
514 message_size_limit = 1;
515 persistence_file = 1;
516 persistence_location = 1;
517 persistent_client_expiration = 1;
519 queue_qos0_messages = 1;
520 retain_available = 1;
523 upgrade_outgoing_qos = 1;
524 websockets_headers_size = 1;
525 websockets_log_level = 1;
528 globalOptions = with lib.types; {
529 enable = lib.mkEnableOption "the MQTT Mosquitto broker";
531 package = lib.mkPackageOption pkgs "mosquitto" { };
533 bridges = lib.mkOption {
534 type = attrsOf bridgeOptions;
537 Bridges to build to other MQTT brokers.
541 listeners = lib.mkOption {
542 type = listOf listenerOptions;
545 Listeners to configure on this broker.
549 includeDirs = lib.mkOption {
552 Directories to be scanned for further config files to include.
553 Directories will processed in the order given,
554 `*.conf` files in the directory will be
555 read in case-sensitive alphabetical order.
560 logDest = lib.mkOption {
571 Destinations to send log messages to.
573 default = [ "stderr" ];
576 logType = lib.mkOption {
577 type = listOf (enum [
590 Types of messages to log.
595 persistence = lib.mkOption {
598 Enable persistent storage of subscriptions and messages.
603 dataDir = lib.mkOption {
604 default = "/var/lib/mosquitto";
605 type = lib.types.path;
611 settings = lib.mkOption {
613 freeformType = attrsOf optionType;
616 Global configuration options for the mosquitto broker.
625 (assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
626 (lib.imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
627 (lib.mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
633 "per_listener_settings true"
634 "persistence ${optionToString cfg.persistence}"
636 ++ map (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}") cfg.logDest
637 ++ map (t: "log_type ${t}") cfg.logType
638 ++ formatFreeform { } cfg.settings
639 ++ lib.concatLists (lib.imap0 formatListener cfg.listeners)
640 ++ lib.concatLists (lib.mapAttrsToList formatBridge cfg.bridges)
641 ++ map (d: "include_dir ${d}") cfg.includeDirs;
643 configFile = pkgs.writeText "mosquitto.conf" (lib.concatStringsSep "\n" (formatGlobal cfg));
651 options.services.mosquitto = globalOptions;
653 ###### Implementation
655 config = lib.mkIf cfg.enable {
657 assertions = globalAsserts "services.mosquitto" cfg;
659 systemd.services.mosquitto = {
660 description = "Mosquitto MQTT Broker Daemon";
661 wantedBy = [ "multi-user.target" ];
662 wants = [ "network-online.target" ];
663 after = [ "network-online.target" ];
666 NotifyAccess = "main";
669 RuntimeDirectory = "mosquitto";
670 WorkingDirectory = cfg.dataDir;
671 Restart = "on-failure";
672 ExecStart = "${cfg.package}/bin/mosquitto -c ${configFile}";
673 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
678 listenerCredentials =
679 listenerScope: listener:
680 usersCredentials listenerScope listener.users [
685 systemdCredentials cfg.listeners listenerCredentials;
689 listenerCredentials =
690 listenerScope: listener:
691 usersCredentials listenerScope listener.users [
696 systemdCredentials cfg.listeners listenerCredentials;
699 CapabilityBoundingSet = "";
700 DevicePolicy = "closed";
701 LockPersonality = true;
702 MemoryDenyWriteExecute = true;
703 NoNewPrivileges = true;
704 PrivateDevices = true;
708 ProtectControlGroups = true;
710 ProtectHostname = true;
711 ProtectKernelLogs = true;
712 ProtectKernelModules = true;
713 ProtectKernelTunables = true;
714 ProtectProc = "invisible";
716 ProtectSystem = "strict";
719 "/tmp" # mosquitto_passwd creates files in /tmp before moving them
720 ] ++ lib.filter path.check cfg.logDest;
721 ReadOnlyPaths = map (p: "${p}") (
723 ++ lib.filter (v: v != null) (
726 (l.settings.psk_file or null)
727 (l.settings.http_dir or null)
728 (l.settings.cafile or null)
729 (l.settings.capath or null)
730 (l.settings.certfile or null)
731 (l.settings.crlfile or null)
732 (l.settings.dhparamfile or null)
733 (l.settings.keyfile or null)
735 (lib.mapAttrsToList (_: b: [
736 (b.settings.bridge_cafile or null)
737 (b.settings.bridge_capath or null)
738 (b.settings.bridge_certfile or null)
739 (b.settings.bridge_keyfile or null)
745 RestrictAddressFamilies = [
751 RestrictNamespaces = true;
752 RestrictRealtime = true;
753 RestrictSUIDSGID = true;
754 SystemCallArchitectures = "native";
762 preStart = lib.concatStringsSep "\n" (
765 makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}"
770 environment.etc = lib.listToAttrs (
771 lib.imap0 (idx: listener: {
772 name = "mosquitto/acl-${toString idx}.conf";
774 user = config.users.users.mosquitto.name;
775 group = config.users.users.mosquitto.group;
778 lib.concatStringsSep "\n" (
781 (lib.mapAttrsToList (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl) listener.users)
789 users.users.mosquitto = {
790 description = "Mosquitto MQTT Broker Daemon owner";
792 uid = config.ids.uids.mosquitto;
797 users.groups.mosquitto.gid = config.ids.gids.mosquitto;
803 doc = ./mosquitto.md;