Merge: zmap: 4.2.0 -> 4.3.1 (#364578)
[NixPkgs.git] / nixos / modules / services / networking / mosquitto.nix
blob9753d0a4a5426d52abab93ac821a04815ca18825
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
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";
15   };
16   path = lib.types.addCheck lib.types.path (p: str.check "${p}");
17   configKey = lib.types.strMatching "[^\r\n\t ]+";
18   optionType =
19     with lib.types;
20     oneOf [
21       str
22       path
23       bool
24       int
25     ]
26     // {
27       description = "string, path, bool, or integer";
28     };
29   optionToString =
30     v:
31     if lib.isBool v then
32       lib.boolToString v
33     else if path.check v then
34       "${v}"
35     else
36       toString v;
38   assertKeysValid =
39     prefix: valid: config:
40     lib.mapAttrsToList (n: _: {
41       assertion = valid ? ${n};
42       message = "Invalid config key ${prefix}.${n}.";
43     }) config;
45   formatFreeform =
46     {
47       prefix ? "",
48     }:
49     lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
51   userOptions =
52     with lib.types;
53     submodule {
54       options = {
55         password = lib.mkOption {
56           type = uniq (nullOr str);
57           default = null;
58           description = ''
59             Specifies the (clear text) password for the MQTT User.
60           '';
61         };
63         passwordFile = lib.mkOption {
64           type = uniq (nullOr path);
65           example = "/path/to/file";
66           default = null;
67           description = ''
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.
73           '';
74         };
76         hashedPassword = lib.mkOption {
77           type = uniq (nullOr str);
78           default = null;
79           description = ''
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
84             file.
85           '';
86         };
88         hashedPasswordFile = lib.mkOption {
89           type = uniq (nullOr path);
90           example = "/path/to/file";
91           default = null;
92           description = ''
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.
101           '';
102         };
104         acl = lib.mkOption {
105           type = listOf str;
106           example = [
107             "read A/B"
108             "readwrite A/#"
109           ];
110           default = [ ];
111           description = ''
112             Control client access to topics on the broker.
113           '';
114         };
115       };
116     };
118   userAsserts =
119     prefix: users:
120     lib.mapAttrsToList (n: _: {
121       assertion = builtins.match "[^:\r\n]+" n != null;
122       message = "Invalid user name ${n} in ${prefix}";
123     }) users
124     ++ lib.mapAttrsToList (n: u: {
125       assertion =
126         lib.count (s: s != null) [
127           u.password
128           u.passwordFile
129           u.hashedPassword
130           u.hashedPasswordFile
131         ] <= 1;
132       message = "Cannot set more than one password option for user ${n} in ${prefix}";
133     }) users;
135   listenerScope = index: "listener-${toString index}";
136   userScope = prefix: index: "${prefix}-user-${toString index}";
137   credentialID = prefix: credential: "${prefix}-${credential}";
139   toScopedUsers =
140     listenerScope: users:
141     lib.pipe users [
142       lib.attrNames
143       (lib.imap0 (
144         index: user: lib.nameValuePair user (users.${user} // { scope = userScope listenerScope index; })
145       ))
146       lib.listToAttrs
147     ];
149   userCredentials =
150     user: credentials:
151     lib.pipe credentials [
152       (lib.filter (credential: user.${credential} != null))
153       (map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
154     ];
155   usersCredentials =
156     listenerScope: users: credentials:
157     lib.pipe users [
158       (toScopedUsers listenerScope)
159       (lib.mapAttrsToList (_: user: userCredentials user credentials))
160       lib.concatLists
161     ];
162   systemdCredentials =
163     listeners: listenerCredentials:
164     lib.pipe listeners [
165       (lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
166       lib.concatLists
167     ];
169   makePasswordFile =
170     listenerScope: users: path:
171     let
172       makeLines =
173         store: file:
174         let
175           scopedUsers = toScopedUsers listenerScope users;
176         in
177         lib.mapAttrsToList (
178           name: user:
179           ''addLine ${lib.escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"''
180         ) (lib.filterAttrs (_: user: user.${store} != null) scopedUsers)
181         ++ lib.mapAttrsToList (
182           name: user:
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";
187     in
188     pkgs.writeScript "make-mosquitto-passwd" (
189       ''
190         #! ${pkgs.runtimeShell}
192         set -eu
194         file=${lib.escapeShellArg path}
196         rm -f "$file"
197         touch "$file"
199         addLine() {
200           echo "$1:$2" >> "$file"
201         }
202         addFile() {
203           if [ $(wc -l <"$2") -gt 1 ]; then
204             echo "invalid mosquitto password file $2" >&2
205             return 1
206           fi
207           echo "$1:$(cat "$2")" >> "$file"
208         }
209       ''
210       + lib.concatStringsSep "\n" (
211         plainLines
212         ++ lib.optional (plainLines != [ ]) ''
213           ${cfg.package}/bin/mosquitto_passwd -U "$file"
214         ''
215         ++ hashedLines
216       )
217     );
219   authPluginOptions =
220     with lib.types;
221     submodule {
222       options = {
223         plugin = lib.mkOption {
224           type = path;
225           description = ''
226             Plugin path to load, should be a `.so` file.
227           '';
228         };
230         denySpecialChars = lib.mkOption {
231           type = bool;
232           description = ''
233             Automatically disallow all clients using `#`
234             or `+` in their name/id.
235           '';
236           default = true;
237         };
239         options = lib.mkOption {
240           type = attrsOf optionType;
241           description = ''
242             Options for the auth plugin. Each key turns into a `auth_opt_*`
243              line in the config.
244           '';
245           default = { };
246         };
247       };
248     };
250   authAsserts =
251     prefix: auth:
252     lib.mapAttrsToList (n: _: {
253       assertion = configKey.check n;
254       message = "Invalid auth plugin key ${prefix}.${n}";
255     }) auth;
257   formatAuthPlugin =
258     plugin:
259     [
260       "auth_plugin ${plugin.plugin}"
261       "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
262     ]
263     ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
265   freeformListenerKeys = {
266     allow_anonymous = 1;
267     allow_zero_length_clientid = 1;
268     auto_id_prefix = 1;
269     bind_interface = 1;
270     cafile = 1;
271     capath = 1;
272     certfile = 1;
273     ciphers = 1;
274     "ciphers_tls1.3" = 1;
275     crlfile = 1;
276     dhparamfile = 1;
277     http_dir = 1;
278     keyfile = 1;
279     max_connections = 1;
280     max_qos = 1;
281     max_topic_alias = 1;
282     mount_point = 1;
283     protocol = 1;
284     psk_file = 1;
285     psk_hint = 1;
286     require_certificate = 1;
287     socket_domain = 1;
288     tls_engine = 1;
289     tls_engine_kpass_sha1 = 1;
290     tls_keyform = 1;
291     tls_version = 1;
292     use_identity_as_username = 1;
293     use_subject_as_username = 1;
294     use_username_as_clientid = 1;
295   };
297   listenerOptions =
298     with lib.types;
299     submodule {
300       options = {
301         port = lib.mkOption {
302           type = port;
303           description = ''
304             Port to listen on. Must be set to 0 to listen on a unix domain socket.
305           '';
306           default = 1883;
307         };
309         address = lib.mkOption {
310           type = nullOr str;
311           description = ''
312             Address to listen on. Listen on `0.0.0.0`/`::`
313             when unset.
314           '';
315           default = null;
316         };
318         authPlugins = lib.mkOption {
319           type = listOf authPluginOptions;
320           description = ''
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.
324           '';
325           default = [ ];
326         };
328         users = lib.mkOption {
329           type = attrsOf userOptions;
330           example = {
331             john = {
332               password = "123456";
333               acl = [ "readwrite john/#" ];
334             };
335           };
336           description = ''
337             A set of users and their passwords and ACLs.
338           '';
339           default = { };
340         };
342         omitPasswordAuth = lib.mkOption {
343           type = bool;
344           description = ''
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.
347           '';
348           default = false;
349         };
351         acl = lib.mkOption {
352           type = listOf str;
353           description = ''
354             Additional ACL items to prepend to the generated ACL file.
355           '';
356           example = [
357             "pattern read #"
358             "topic readwrite anon/report/#"
359           ];
360           default = [ ];
361         };
363         settings = lib.mkOption {
364           type = submodule {
365             freeformType = attrsOf optionType;
366           };
367           description = ''
368             Additional settings for this listener.
369           '';
370           default = { };
371         };
372       };
373     };
375   listenerAsserts =
376     prefix: 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;
381   formatListener =
382     idx: listener:
383     [
384       "listener ${toString listener.port} ${toString listener.address}"
385       "acl_file /etc/mosquitto/acl-${toString idx}.conf"
386     ]
387     ++ lib.optional (!listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
388     ++ formatFreeform { } listener.settings
389     ++ lib.concatMap formatAuthPlugin listener.authPlugins;
391   freeformBridgeKeys = {
392     bridge_alpn = 1;
393     bridge_attempt_unsubscribe = 1;
394     bridge_bind_address = 1;
395     bridge_cafile = 1;
396     bridge_capath = 1;
397     bridge_certfile = 1;
398     bridge_identity = 1;
399     bridge_insecure = 1;
400     bridge_keyfile = 1;
401     bridge_max_packet_size = 1;
402     bridge_outgoing_retain = 1;
403     bridge_protocol_version = 1;
404     bridge_psk = 1;
405     bridge_require_ocsp = 1;
406     bridge_tls_version = 1;
407     cleansession = 1;
408     idle_timeout = 1;
409     keepalive_interval = 1;
410     local_cleansession = 1;
411     local_clientid = 1;
412     local_password = 1;
413     local_username = 1;
414     notification_topic = 1;
415     notifications = 1;
416     notifications_local_only = 1;
417     remote_clientid = 1;
418     remote_password = 1;
419     remote_username = 1;
420     restart_timeout = 1;
421     round_robin = 1;
422     start_type = 1;
423     threshold = 1;
424     try_private = 1;
425   };
427   bridgeOptions =
428     with lib.types;
429     submodule {
430       options = {
431         addresses = lib.mkOption {
432           type = listOf (submodule {
433             options = {
434               address = lib.mkOption {
435                 type = str;
436                 description = ''
437                   Address of the remote MQTT broker.
438                 '';
439               };
441               port = lib.mkOption {
442                 type = port;
443                 description = ''
444                   Port of the remote MQTT broker.
445                 '';
446                 default = 1883;
447               };
448             };
449           });
450           default = [ ];
451           description = ''
452             Remote endpoints for the bridge.
453           '';
454         };
456         topics = lib.mkOption {
457           type = listOf str;
458           description = ''
459             Topic patterns to be shared between the two brokers.
460             Refer to the [
461             mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
462           '';
463           default = [ ];
464           example = [ "# both 2 local/topic/ remote/topic/" ];
465         };
467         settings = lib.mkOption {
468           type = submodule {
469             freeformType = attrsOf optionType;
470           };
471           description = ''
472             Additional settings for this bridge.
473           '';
474           default = { };
475         };
476       };
477     };
479   bridgeAsserts =
480     prefix: bridge:
481     assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
482     ++ [
483       {
484         assertion = lib.length bridge.addresses > 0;
485         message = "Bridge ${prefix} needs remote broker addresses";
486       }
487     ];
489   formatBridge =
490     name: bridge:
491     [
492       "connection ${name}"
493       "addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
494     ]
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;
504     log_facility = 1;
505     log_timestamp = 1;
506     log_timestamp_format = 1;
507     max_inflight_bytes = 1;
508     max_inflight_messages = 1;
509     max_keepalive = 1;
510     max_packet_size = 1;
511     max_queued_bytes = 1;
512     max_queued_messages = 1;
513     memory_limit = 1;
514     message_size_limit = 1;
515     persistence_file = 1;
516     persistence_location = 1;
517     persistent_client_expiration = 1;
518     pid_file = 1;
519     queue_qos0_messages = 1;
520     retain_available = 1;
521     set_tcp_nodelay = 1;
522     sys_interval = 1;
523     upgrade_outgoing_qos = 1;
524     websockets_headers_size = 1;
525     websockets_log_level = 1;
526   };
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;
535       default = { };
536       description = ''
537         Bridges to build to other MQTT brokers.
538       '';
539     };
541     listeners = lib.mkOption {
542       type = listOf listenerOptions;
543       default = [ ];
544       description = ''
545         Listeners to configure on this broker.
546       '';
547     };
549     includeDirs = lib.mkOption {
550       type = listOf path;
551       description = ''
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.
556       '';
557       default = [ ];
558     };
560     logDest = lib.mkOption {
561       type = listOf (
562         either path (enum [
563           "stdout"
564           "stderr"
565           "syslog"
566           "topic"
567           "dlt"
568         ])
569       );
570       description = ''
571         Destinations to send log messages to.
572       '';
573       default = [ "stderr" ];
574     };
576     logType = lib.mkOption {
577       type = listOf (enum [
578         "debug"
579         "error"
580         "warning"
581         "notice"
582         "information"
583         "subscribe"
584         "unsubscribe"
585         "websockets"
586         "none"
587         "all"
588       ]);
589       description = ''
590         Types of messages to log.
591       '';
592       default = [ ];
593     };
595     persistence = lib.mkOption {
596       type = bool;
597       description = ''
598         Enable persistent storage of subscriptions and messages.
599       '';
600       default = true;
601     };
603     dataDir = lib.mkOption {
604       default = "/var/lib/mosquitto";
605       type = lib.types.path;
606       description = ''
607         The data directory.
608       '';
609     };
611     settings = lib.mkOption {
612       type = submodule {
613         freeformType = attrsOf optionType;
614       };
615       description = ''
616         Global configuration options for the mosquitto broker.
617       '';
618       default = { };
619     };
620   };
622   globalAsserts =
623     prefix: cfg:
624     lib.flatten [
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)
628     ];
630   formatGlobal =
631     cfg:
632     [
633       "per_listener_settings true"
634       "persistence ${optionToString cfg.persistence}"
635     ]
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));
649   ###### Interface
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" ];
664       serviceConfig = {
665         Type = "notify";
666         NotifyAccess = "main";
667         User = "mosquitto";
668         Group = "mosquitto";
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";
675         # Credentials
676         SetCredential =
677           let
678             listenerCredentials =
679               listenerScope: listener:
680               usersCredentials listenerScope listener.users [
681                 "password"
682                 "hashedPassword"
683               ];
684           in
685           systemdCredentials cfg.listeners listenerCredentials;
687         LoadCredential =
688           let
689             listenerCredentials =
690               listenerScope: listener:
691               usersCredentials listenerScope listener.users [
692                 "passwordFile"
693                 "hashedPasswordFile"
694               ];
695           in
696           systemdCredentials cfg.listeners listenerCredentials;
698         # Hardening
699         CapabilityBoundingSet = "";
700         DevicePolicy = "closed";
701         LockPersonality = true;
702         MemoryDenyWriteExecute = true;
703         NoNewPrivileges = true;
704         PrivateDevices = true;
705         PrivateTmp = true;
706         PrivateUsers = true;
707         ProtectClock = true;
708         ProtectControlGroups = true;
709         ProtectHome = true;
710         ProtectHostname = true;
711         ProtectKernelLogs = true;
712         ProtectKernelModules = true;
713         ProtectKernelTunables = true;
714         ProtectProc = "invisible";
715         ProcSubset = "pid";
716         ProtectSystem = "strict";
717         ReadWritePaths = [
718           cfg.dataDir
719           "/tmp" # mosquitto_passwd creates files in /tmp before moving them
720         ] ++ lib.filter path.check cfg.logDest;
721         ReadOnlyPaths = map (p: "${p}") (
722           cfg.includeDirs
723           ++ lib.filter (v: v != null) (
724             lib.flatten [
725               (map (l: [
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)
734               ]) cfg.listeners)
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)
740               ]) cfg.bridges)
741             ]
742           )
743         );
744         RemoveIPC = true;
745         RestrictAddressFamilies = [
746           "AF_UNIX"
747           "AF_INET"
748           "AF_INET6"
749           "AF_NETLINK"
750         ];
751         RestrictNamespaces = true;
752         RestrictRealtime = true;
753         RestrictSUIDSGID = true;
754         SystemCallArchitectures = "native";
755         SystemCallFilter = [
756           "@system-service"
757           "~@privileged"
758           "~@resources"
759         ];
760         UMask = "0077";
761       };
762       preStart = lib.concatStringsSep "\n" (
763         lib.imap0 (
764           idx: listener:
765           makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}"
766         ) cfg.listeners
767       );
768     };
770     environment.etc = lib.listToAttrs (
771       lib.imap0 (idx: listener: {
772         name = "mosquitto/acl-${toString idx}.conf";
773         value = {
774           user = config.users.users.mosquitto.name;
775           group = config.users.users.mosquitto.group;
776           mode = "0400";
777           text = (
778             lib.concatStringsSep "\n" (
779               lib.flatten [
780                 listener.acl
781                 (lib.mapAttrsToList (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl) listener.users)
782               ]
783             )
784           );
785         };
786       }) cfg.listeners
787     );
789     users.users.mosquitto = {
790       description = "Mosquitto MQTT Broker Daemon owner";
791       group = "mosquitto";
792       uid = config.ids.uids.mosquitto;
793       home = cfg.dataDir;
794       createHome = true;
795     };
797     users.groups.mosquitto.gid = config.ids.gids.mosquitto;
799   };
801   meta = {
802     maintainers = [ ];
803     doc = ./mosquitto.md;
804   };