1 { config, lib, pkgs, ...}:
3 cfg = config.services.mosquitto;
5 # note that mosquitto config parsing is very simplistic as of may 2021.
6 # often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
7 # there's no escaping available either, so we have to prevent any being necessary.
8 str = lib.types.strMatching "[^\r\n]*" // {
9 description = "single-line string";
11 path = lib.types.addCheck lib.types.path (p: str.check "${p}");
12 configKey = lib.types.strMatching "[^\r\n\t ]+";
13 optionType = with lib.types; oneOf [ str path bool int ] // {
14 description = "string, path, bool, or integer";
17 if lib.isBool v then lib.boolToString v
18 else if path.check v then "${v}"
21 assertKeysValid = prefix: valid: config:
24 assertion = valid ? ${n};
25 message = "Invalid config key ${prefix}.${n}.";
29 formatFreeform = { prefix ? "" }: lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
31 userOptions = with lib.types; submodule {
33 password = lib.mkOption {
34 type = uniq (nullOr str);
37 Specifies the (clear text) password for the MQTT User.
41 passwordFile = lib.mkOption {
42 type = uniq (nullOr path);
43 example = "/path/to/file";
46 Specifies the path to a file containing the
47 clear text password for the MQTT user.
48 The file is securely passed to mosquitto by
49 leveraging systemd credentials. No special
50 permissions need to be set on this file.
54 hashedPassword = lib.mkOption {
55 type = uniq (nullOr str);
58 Specifies the hashed password for the MQTT User.
59 To generate hashed password install the `mosquitto`
60 package and use `mosquitto_passwd`, then extract
61 the second field (after the `:`) from the generated
66 hashedPasswordFile = lib.mkOption {
67 type = uniq (nullOr path);
68 example = "/path/to/file";
71 Specifies the path to a file containing the
72 hashed password for the MQTT user.
73 To generate hashed password install the `mosquitto`
74 package and use `mosquitto_passwd`, then remove the
75 `username:` prefix from the generated file.
76 The file is securely passed to mosquitto by
77 leveraging systemd credentials. No special
78 permissions need to be set on this file.
84 example = [ "read A/B" "readwrite A/#" ];
87 Control client access to topics on the broker.
93 userAsserts = prefix: users:
96 assertion = builtins.match "[^:\r\n]+" n != null;
97 message = "Invalid user name ${n} in ${prefix}";
100 ++ lib.mapAttrsToList
102 assertion = lib.count (s: s != null) [
103 u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
105 message = "Cannot set more than one password option for user ${n} in ${prefix}";
108 listenerScope = index: "listener-${toString index}";
109 userScope = prefix: index: "${prefix}-user-${toString index}";
110 credentialID = prefix: credential: "${prefix}-${credential}";
112 toScopedUsers = listenerScope: users: lib.pipe users [
114 (lib.imap0 (index: user: lib.nameValuePair user
115 (users.${user} // { scope = userScope listenerScope index; })
120 userCredentials = user: credentials: lib.pipe credentials [
121 (lib.filter (credential: user.${credential} != null))
122 (map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
124 usersCredentials = listenerScope: users: credentials: lib.pipe users [
125 (toScopedUsers listenerScope)
126 (lib.mapAttrsToList (_: user: userCredentials user credentials))
129 systemdCredentials = listeners: listenerCredentials: lib.pipe listeners [
130 (lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
134 makePasswordFile = listenerScope: users: path:
136 makeLines = store: file: let
137 scopedUsers = toScopedUsers listenerScope users;
140 (name: user: ''addLine ${lib.escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"'')
141 (lib.filterAttrs (_: user: user.${store} != null) scopedUsers)
142 ++ lib.mapAttrsToList
143 (name: user: ''addFile ${lib.escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"'')
144 (lib.filterAttrs (_: user: user.${file} != null) scopedUsers);
145 plainLines = makeLines "password" "passwordFile";
146 hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
148 pkgs.writeScript "make-mosquitto-passwd"
150 #! ${pkgs.runtimeShell}
154 file=${lib.escapeShellArg path}
160 echo "$1:$2" >> "$file"
163 if [ $(wc -l <"$2") -gt 1 ]; then
164 echo "invalid mosquitto password file $2" >&2
167 echo "$1:$(cat "$2")" >> "$file"
170 + lib.concatStringsSep "\n"
172 ++ lib.optional (plainLines != []) ''
173 ${cfg.package}/bin/mosquitto_passwd -U "$file"
177 authPluginOptions = with lib.types; submodule {
179 plugin = lib.mkOption {
182 Plugin path to load, should be a `.so` file.
186 denySpecialChars = lib.mkOption {
189 Automatically disallow all clients using `#`
190 or `+` in their name/id.
195 options = lib.mkOption {
196 type = attrsOf optionType;
198 Options for the auth plugin. Each key turns into a `auth_opt_*`
206 authAsserts = prefix: auth:
209 assertion = configKey.check n;
210 message = "Invalid auth plugin key ${prefix}.${n}";
214 formatAuthPlugin = plugin:
216 "auth_plugin ${plugin.plugin}"
217 "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
219 ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
221 freeformListenerKeys = {
223 allow_zero_length_clientid = 1;
230 "ciphers_tls1.3" = 1;
242 require_certificate = 1;
245 tls_engine_kpass_sha1 = 1;
248 use_identity_as_username = 1;
249 use_subject_as_username = 1;
250 use_username_as_clientid = 1;
253 listenerOptions = with lib.types; submodule {
255 port = lib.mkOption {
258 Port to listen on. Must be set to 0 to listen on a unix domain socket.
263 address = lib.mkOption {
266 Address to listen on. Listen on `0.0.0.0`/`::`
272 authPlugins = lib.mkOption {
273 type = listOf authPluginOptions;
275 Authentication plugin to attach to this listener.
276 Refer to the [mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html)
277 for details on authentication plugins.
282 users = lib.mkOption {
283 type = attrsOf userOptions;
284 example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
286 A set of users and their passwords and ACLs.
291 omitPasswordAuth = lib.mkOption {
294 Omits password checking, allowing anyone to log in with any user name unless
295 other mandatory authentication methods (eg TLS client certificates) are configured.
303 Additional ACL items to prepend to the generated ACL file.
305 example = [ "pattern read #" "topic readwrite anon/report/#" ];
309 settings = lib.mkOption {
311 freeformType = attrsOf optionType;
314 Additional settings for this listener.
321 listenerAsserts = prefix: listener:
322 assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
323 ++ userAsserts prefix listener.users
325 (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
326 listener.authPlugins;
328 formatListener = idx: listener:
330 "listener ${toString listener.port} ${toString listener.address}"
331 "acl_file /etc/mosquitto/acl-${toString idx}.conf"
333 ++ lib.optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
334 ++ formatFreeform {} listener.settings
335 ++ lib.concatMap formatAuthPlugin listener.authPlugins;
337 freeformBridgeKeys = {
339 bridge_attempt_unsubscribe = 1;
340 bridge_bind_address = 1;
347 bridge_max_packet_size = 1;
348 bridge_outgoing_retain = 1;
349 bridge_protocol_version = 1;
351 bridge_require_ocsp = 1;
352 bridge_tls_version = 1;
355 keepalive_interval = 1;
356 local_cleansession = 1;
360 notification_topic = 1;
362 notifications_local_only = 1;
373 bridgeOptions = with lib.types; submodule {
375 addresses = lib.mkOption {
376 type = listOf (submodule {
378 address = lib.mkOption {
381 Address of the remote MQTT broker.
385 port = lib.mkOption {
388 Port of the remote MQTT broker.
396 Remote endpoints for the bridge.
400 topics = lib.mkOption {
403 Topic patterns to be shared between the two brokers.
405 mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
408 example = [ "# both 2 local/topic/ remote/topic/" ];
411 settings = lib.mkOption {
413 freeformType = attrsOf optionType;
416 Additional settings for this bridge.
423 bridgeAsserts = prefix: bridge:
424 assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
426 assertion = lib.length bridge.addresses > 0;
427 message = "Bridge ${prefix} needs remote broker addresses";
430 formatBridge = name: bridge:
433 "addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
435 ++ map (t: "topic ${t}") bridge.topics
436 ++ formatFreeform {} bridge.settings;
438 freeformGlobalKeys = {
439 allow_duplicate_messages = 1;
440 autosave_interval = 1;
441 autosave_on_changes = 1;
442 check_retain_source = 1;
443 connection_messages = 1;
446 log_timestamp_format = 1;
447 max_inflight_bytes = 1;
448 max_inflight_messages = 1;
451 max_queued_bytes = 1;
452 max_queued_messages = 1;
454 message_size_limit = 1;
455 persistence_file = 1;
456 persistence_location = 1;
457 persistent_client_expiration = 1;
459 queue_qos0_messages = 1;
460 retain_available = 1;
463 upgrade_outgoing_qos = 1;
464 websockets_headers_size = 1;
465 websockets_log_level = 1;
468 globalOptions = with lib.types; {
469 enable = lib.mkEnableOption "the MQTT Mosquitto broker";
471 package = lib.mkPackageOption pkgs "mosquitto" { };
473 bridges = lib.mkOption {
474 type = attrsOf bridgeOptions;
477 Bridges to build to other MQTT brokers.
481 listeners = lib.mkOption {
482 type = listOf listenerOptions;
485 Listeners to configure on this broker.
489 includeDirs = lib.mkOption {
492 Directories to be scanned for further config files to include.
493 Directories will processed in the order given,
494 `*.conf` files in the directory will be
495 read in case-sensitive alphabetical order.
500 logDest = lib.mkOption {
501 type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
503 Destinations to send log messages to.
505 default = [ "stderr" ];
508 logType = lib.mkOption {
509 type = listOf (enum [ "debug" "error" "warning" "notice" "information"
510 "subscribe" "unsubscribe" "websockets" "none" "all" ]);
512 Types of messages to log.
517 persistence = lib.mkOption {
520 Enable persistent storage of subscriptions and messages.
525 dataDir = lib.mkOption {
526 default = "/var/lib/mosquitto";
527 type = lib.types.path;
533 settings = lib.mkOption {
535 freeformType = attrsOf optionType;
538 Global configuration options for the mosquitto broker.
544 globalAsserts = prefix: cfg:
546 (assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
547 (lib.imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
548 (lib.mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
553 "per_listener_settings true"
554 "persistence ${optionToString cfg.persistence}"
557 (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
559 ++ map (t: "log_type ${t}") cfg.logType
560 ++ formatFreeform {} cfg.settings
561 ++ lib.concatLists (lib.imap0 formatListener cfg.listeners)
562 ++ lib.concatLists (lib.mapAttrsToList formatBridge cfg.bridges)
563 ++ map (d: "include_dir ${d}") cfg.includeDirs;
565 configFile = pkgs.writeText "mosquitto.conf"
566 (lib.concatStringsSep "\n" (formatGlobal cfg));
574 options.services.mosquitto = globalOptions;
576 ###### Implementation
578 config = lib.mkIf cfg.enable {
580 assertions = globalAsserts "services.mosquitto" cfg;
582 systemd.services.mosquitto = {
583 description = "Mosquitto MQTT Broker Daemon";
584 wantedBy = [ "multi-user.target" ];
585 wants = [ "network-online.target" ];
586 after = [ "network-online.target" ];
589 NotifyAccess = "main";
592 RuntimeDirectory = "mosquitto";
593 WorkingDirectory = cfg.dataDir;
594 Restart = "on-failure";
595 ExecStart = "${cfg.package}/bin/mosquitto -c ${configFile}";
596 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
600 listenerCredentials = listenerScope: listener:
601 usersCredentials listenerScope listener.users [ "password" "hashedPassword" ];
603 systemdCredentials cfg.listeners listenerCredentials;
606 listenerCredentials = listenerScope: listener:
607 usersCredentials listenerScope listener.users [ "passwordFile" "hashedPasswordFile" ];
609 systemdCredentials cfg.listeners listenerCredentials;
612 CapabilityBoundingSet = "";
613 DevicePolicy = "closed";
614 LockPersonality = true;
615 MemoryDenyWriteExecute = true;
616 NoNewPrivileges = true;
617 PrivateDevices = true;
621 ProtectControlGroups = true;
623 ProtectHostname = true;
624 ProtectKernelLogs = true;
625 ProtectKernelModules = true;
626 ProtectKernelTunables = true;
627 ProtectProc = "invisible";
629 ProtectSystem = "strict";
632 "/tmp" # mosquitto_passwd creates files in /tmp before moving them
633 ] ++ lib.filter path.check cfg.logDest;
642 (l.settings.psk_file or null)
643 (l.settings.http_dir or null)
644 (l.settings.cafile or null)
645 (l.settings.capath or null)
646 (l.settings.certfile or null)
647 (l.settings.crlfile or null)
648 (l.settings.dhparamfile or null)
649 (l.settings.keyfile or null)
654 (b.settings.bridge_cafile or null)
655 (b.settings.bridge_capath or null)
656 (b.settings.bridge_certfile or null)
657 (b.settings.bridge_keyfile or null)
662 RestrictAddressFamilies = [
668 RestrictNamespaces = true;
669 RestrictRealtime = true;
670 RestrictSUIDSGID = true;
671 SystemCallArchitectures = "native";
683 (idx: listener: makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}")
687 environment.etc = lib.listToAttrs (
690 name = "mosquitto/acl-${toString idx}.conf";
692 user = config.users.users.mosquitto.name;
693 group = config.users.users.mosquitto.group;
695 text = (lib.concatStringsSep
700 (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
708 users.users.mosquitto = {
709 description = "Mosquitto MQTT Broker Daemon owner";
711 uid = config.ids.uids.mosquitto;
716 users.groups.mosquitto.gid = config.ids.gids.mosquitto;
722 doc = ./mosquitto.md;