1 { config, lib, pkgs, ...}:
6 cfg = config.services.mosquitto;
8 # note that mosquitto config parsing is very simplistic as of may 2021.
9 # often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
10 # there's no escaping available either, so we have to prevent any being necessary.
11 str = types.strMatching "[^\r\n]*" // {
12 description = "single-line string";
14 path = types.addCheck types.path (p: str.check "${p}");
15 configKey = types.strMatching "[^\r\n\t ]+";
16 optionType = with types; oneOf [ str path bool int ] // {
17 description = "string, path, bool, or integer";
20 if isBool v then boolToString v
21 else if path.check v then "${v}"
24 assertKeysValid = prefix: valid: config:
27 assertion = valid ? ${n};
28 message = "Invalid config key ${prefix}.${n}.";
32 formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
34 userOptions = with types; submodule {
37 type = uniq (nullOr str);
39 description = lib.mdDoc ''
40 Specifies the (clear text) password for the MQTT User.
44 passwordFile = mkOption {
45 type = uniq (nullOr types.path);
46 example = "/path/to/file";
48 description = lib.mdDoc ''
49 Specifies the path to a file containing the
50 clear text password for the MQTT user.
54 hashedPassword = mkOption {
55 type = uniq (nullOr str);
57 description = mdDoc ''
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 = mkOption {
67 type = uniq (nullOr types.path);
68 example = "/path/to/file";
70 description = mdDoc ''
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.
81 example = [ "read A/B" "readwrite A/#" ];
83 description = lib.mdDoc ''
84 Control client access to topics on the broker.
90 userAsserts = prefix: users:
93 assertion = builtins.match "[^:\r\n]+" n != null;
94 message = "Invalid user name ${n} in ${prefix}";
99 assertion = count (s: s != null) [
100 u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
102 message = "Cannot set more than one password option for user ${n} in ${prefix}";
105 makePasswordFile = users: path:
107 makeLines = store: file:
109 (n: u: "addLine ${escapeShellArg n} ${escapeShellArg u.${store}}")
110 (filterAttrs (_: u: u.${store} != null) users)
112 (n: u: "addFile ${escapeShellArg n} ${escapeShellArg "${u.${file}}"}")
113 (filterAttrs (_: u: u.${file} != null) users);
114 plainLines = makeLines "password" "passwordFile";
115 hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
117 pkgs.writeScript "make-mosquitto-passwd"
119 #! ${pkgs.runtimeShell}
123 file=${escapeShellArg path}
129 echo "$1:$2" >> "$file"
132 if [ $(wc -l <"$2") -gt 1 ]; then
133 echo "invalid mosquitto password file $2" >&2
136 echo "$1:$(cat "$2")" >> "$file"
139 + concatStringsSep "\n"
141 ++ optional (plainLines != []) ''
142 ${cfg.package}/bin/mosquitto_passwd -U "$file"
146 makeACLFile = idx: users: supplement:
147 pkgs.writeText "mosquitto-acl-${toString idx}.conf"
153 (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
157 authPluginOptions = with types; submodule {
161 description = mdDoc ''
162 Plugin path to load, should be a `.so` file.
166 denySpecialChars = mkOption {
168 description = mdDoc ''
169 Automatically disallow all clients using `#`
170 or `+` in their name/id.
176 type = attrsOf optionType;
177 description = mdDoc ''
178 Options for the auth plugin. Each key turns into a `auth_opt_*`
186 authAsserts = prefix: auth:
189 assertion = configKey.check n;
190 message = "Invalid auth plugin key ${prefix}.${n}";
194 formatAuthPlugin = plugin:
196 "auth_plugin ${plugin.plugin}"
197 "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
199 ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
201 freeformListenerKeys = {
203 allow_zero_length_clientid = 1;
210 "ciphers_tls1.3" = 1;
222 require_certificate = 1;
225 tls_engine_kpass_sha1 = 1;
228 use_identity_as_username = 1;
229 use_subject_as_username = 1;
230 use_username_as_clientid = 1;
233 listenerOptions = with types; submodule {
237 description = lib.mdDoc ''
238 Port to listen on. Must be set to 0 to listen on a unix domain socket.
245 description = mdDoc ''
246 Address to listen on. Listen on `0.0.0.0`/`::`
252 authPlugins = mkOption {
253 type = listOf authPluginOptions;
254 description = mdDoc ''
255 Authentication plugin to attach to this listener.
256 Refer to the [mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html)
257 for details on authentication plugins.
263 type = attrsOf userOptions;
264 example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
265 description = lib.mdDoc ''
266 A set of users and their passwords and ACLs.
271 omitPasswordAuth = mkOption {
273 description = lib.mdDoc ''
274 Omits password checking, allowing anyone to log in with any user name unless
275 other mandatory authentication methods (eg TLS client certificates) are configured.
282 description = lib.mdDoc ''
283 Additional ACL items to prepend to the generated ACL file.
285 example = [ "pattern read #" "topic readwrite anon/report/#" ];
289 settings = mkOption {
291 freeformType = attrsOf optionType;
293 description = lib.mdDoc ''
294 Additional settings for this listener.
301 listenerAsserts = prefix: listener:
302 assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
303 ++ userAsserts prefix listener.users
305 (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
306 listener.authPlugins;
308 formatListener = idx: listener:
310 "listener ${toString listener.port} ${toString listener.address}"
311 "acl_file ${makeACLFile idx listener.users listener.acl}"
313 ++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
314 ++ formatFreeform {} listener.settings
315 ++ concatMap formatAuthPlugin listener.authPlugins;
317 freeformBridgeKeys = {
319 bridge_attempt_unsubscribe = 1;
320 bridge_bind_address = 1;
327 bridge_max_packet_size = 1;
328 bridge_outgoing_retain = 1;
329 bridge_protocol_version = 1;
331 bridge_require_ocsp = 1;
332 bridge_tls_version = 1;
335 keepalive_interval = 1;
336 local_cleansession = 1;
340 notification_topic = 1;
342 notifications_local_only = 1;
353 bridgeOptions = with types; submodule {
355 addresses = mkOption {
356 type = listOf (submodule {
360 description = lib.mdDoc ''
361 Address of the remote MQTT broker.
367 description = lib.mdDoc ''
368 Port of the remote MQTT broker.
375 description = lib.mdDoc ''
376 Remote endpoints for the bridge.
382 description = lib.mdDoc ''
383 Topic patterns to be shared between the two brokers.
385 mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
388 example = [ "# both 2 local/topic/ remote/topic/" ];
391 settings = mkOption {
393 freeformType = attrsOf optionType;
395 description = lib.mdDoc ''
396 Additional settings for this bridge.
403 bridgeAsserts = prefix: bridge:
404 assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
406 assertion = length bridge.addresses > 0;
407 message = "Bridge ${prefix} needs remote broker addresses";
410 formatBridge = name: bridge:
413 "addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
415 ++ map (t: "topic ${t}") bridge.topics
416 ++ formatFreeform {} bridge.settings;
418 freeformGlobalKeys = {
419 allow_duplicate_messages = 1;
420 autosave_interval = 1;
421 autosave_on_changes = 1;
422 check_retain_source = 1;
423 connection_messages = 1;
426 log_timestamp_format = 1;
427 max_inflight_bytes = 1;
428 max_inflight_messages = 1;
431 max_queued_bytes = 1;
432 max_queued_messages = 1;
434 message_size_limit = 1;
435 persistence_file = 1;
436 persistence_location = 1;
437 persistent_client_expiration = 1;
439 queue_qos0_messages = 1;
440 retain_available = 1;
443 upgrade_outgoing_qos = 1;
444 websockets_headers_size = 1;
445 websockets_log_level = 1;
448 globalOptions = with types; {
449 enable = mkEnableOption (lib.mdDoc "the MQTT Mosquitto broker");
453 default = pkgs.mosquitto;
454 defaultText = literalExpression "pkgs.mosquitto";
455 description = lib.mdDoc ''
456 Mosquitto package to use.
461 type = attrsOf bridgeOptions;
463 description = lib.mdDoc ''
464 Bridges to build to other MQTT brokers.
468 listeners = mkOption {
469 type = listOf listenerOptions;
471 description = lib.mdDoc ''
472 Listeners to configure on this broker.
476 includeDirs = mkOption {
478 description = mdDoc ''
479 Directories to be scanned for further config files to include.
480 Directories will processed in the order given,
481 `*.conf` files in the directory will be
482 read in case-sensistive alphabetical order.
488 type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
489 description = lib.mdDoc ''
490 Destinations to send log messages to.
492 default = [ "stderr" ];
496 type = listOf (enum [ "debug" "error" "warning" "notice" "information"
497 "subscribe" "unsubscribe" "websockets" "none" "all" ]);
498 description = lib.mdDoc ''
499 Types of messages to log.
504 persistence = mkOption {
506 description = lib.mdDoc ''
507 Enable persistent storage of subscriptions and messages.
513 default = "/var/lib/mosquitto";
515 description = lib.mdDoc ''
520 settings = mkOption {
522 freeformType = attrsOf optionType;
524 description = lib.mdDoc ''
525 Global configuration options for the mosquitto broker.
531 globalAsserts = prefix: cfg:
533 (assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
534 (imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
535 (mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
540 "per_listener_settings true"
541 "persistence ${optionToString cfg.persistence}"
544 (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
546 ++ map (t: "log_type ${t}") cfg.logType
547 ++ formatFreeform {} cfg.settings
548 ++ concatLists (imap0 formatListener cfg.listeners)
549 ++ concatLists (mapAttrsToList formatBridge cfg.bridges)
550 ++ map (d: "include_dir ${d}") cfg.includeDirs;
552 configFile = pkgs.writeText "mosquitto.conf"
553 (concatStringsSep "\n" (formatGlobal cfg));
561 options.services.mosquitto = globalOptions;
563 ###### Implementation
565 config = mkIf cfg.enable {
567 assertions = globalAsserts "services.mosquitto" cfg;
569 systemd.services.mosquitto = {
570 description = "Mosquitto MQTT Broker Daemon";
571 wantedBy = [ "multi-user.target" ];
572 after = [ "network-online.target" ];
575 NotifyAccess = "main";
578 RuntimeDirectory = "mosquitto";
579 WorkingDirectory = cfg.dataDir;
580 Restart = "on-failure";
581 ExecStart = "${cfg.package}/bin/mosquitto -c ${configFile}";
582 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
585 CapabilityBoundingSet = "";
586 DevicePolicy = "closed";
587 LockPersonality = true;
588 MemoryDenyWriteExecute = true;
589 NoNewPrivileges = true;
590 PrivateDevices = true;
594 ProtectControlGroups = true;
596 ProtectHostname = true;
597 ProtectKernelLogs = true;
598 ProtectKernelModules = true;
599 ProtectKernelTunables = true;
600 ProtectProc = "invisible";
602 ProtectSystem = "strict";
605 "/tmp" # mosquitto_passwd creates files in /tmp before moving them
606 ] ++ filter path.check cfg.logDest;
615 (l.settings.psk_file or null)
616 (l.settings.http_dir or null)
617 (l.settings.cafile or null)
618 (l.settings.capath or null)
619 (l.settings.certfile or null)
620 (l.settings.crlfile or null)
621 (l.settings.dhparamfile or null)
622 (l.settings.keyfile or null)
627 (b.settings.bridge_cafile or null)
628 (b.settings.bridge_capath or null)
629 (b.settings.bridge_certfile or null)
630 (b.settings.bridge_keyfile or null)
635 RestrictAddressFamilies = [
641 RestrictNamespaces = true;
642 RestrictRealtime = true;
643 RestrictSUIDSGID = true;
644 SystemCallArchitectures = "native";
656 (idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}")
660 users.users.mosquitto = {
661 description = "Mosquitto MQTT Broker Daemon owner";
663 uid = config.ids.uids.mosquitto;
668 users.groups.mosquitto.gid = config.ids.gids.mosquitto;
673 maintainers = with lib.maintainers; [ pennae ];
674 # Don't edit the docbook xml directly, edit the md and generate it:
675 # `pandoc mosquitto.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > mosquitto.xml`
676 doc = ./mosquitto.xml;