vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / mosquitto.nix
blobe628be0d5b1b345387c51e5b48b1374effae4bb7
1 { config, lib, pkgs, ...}:
2 let
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";
10   };
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";
15   };
16   optionToString = v:
17     if lib.isBool v then lib.boolToString v
18     else if path.check v then "${v}"
19     else toString v;
21   assertKeysValid = prefix: valid: config:
22     lib.mapAttrsToList
23       (n: _: {
24         assertion = valid ? ${n};
25         message = "Invalid config key ${prefix}.${n}.";
26       })
27       config;
29   formatFreeform = { prefix ? "" }: lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
31   userOptions = with lib.types; submodule {
32     options = {
33       password = lib.mkOption {
34         type = uniq (nullOr str);
35         default = null;
36         description = ''
37           Specifies the (clear text) password for the MQTT User.
38         '';
39       };
41       passwordFile = lib.mkOption {
42         type = uniq (nullOr path);
43         example = "/path/to/file";
44         default = null;
45         description = ''
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.
51         '';
52       };
54       hashedPassword = lib.mkOption {
55         type = uniq (nullOr str);
56         default = null;
57         description = ''
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
62           file.
63         '';
64       };
66       hashedPasswordFile = lib.mkOption {
67         type = uniq (nullOr path);
68         example = "/path/to/file";
69         default = null;
70         description = ''
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.
79         '';
80       };
82       acl = lib.mkOption {
83         type = listOf str;
84         example = [ "read A/B" "readwrite A/#" ];
85         default = [];
86         description = ''
87           Control client access to topics on the broker.
88         '';
89       };
90     };
91   };
93   userAsserts = prefix: users:
94     lib.mapAttrsToList
95       (n: _: {
96         assertion = builtins.match "[^:\r\n]+" n != null;
97         message = "Invalid user name ${n} in ${prefix}";
98       })
99       users
100     ++ lib.mapAttrsToList
101       (n: u: {
102         assertion = lib.count (s: s != null) [
103           u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
104         ] <= 1;
105         message = "Cannot set more than one password option for user ${n} in ${prefix}";
106       }) users;
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 [
113     lib.attrNames
114     (lib.imap0 (index: user: lib.nameValuePair user
115       (users.${user} // { scope = userScope listenerScope index; })
116     ))
117     lib.listToAttrs
118   ];
120   userCredentials = user: credentials: lib.pipe credentials [
121     (lib.filter (credential: user.${credential} != null))
122     (map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
123   ];
124   usersCredentials = listenerScope: users: credentials: lib.pipe users [
125     (toScopedUsers listenerScope)
126     (lib.mapAttrsToList (_: user: userCredentials user credentials))
127     lib.concatLists
128   ];
129   systemdCredentials = listeners: listenerCredentials: lib.pipe listeners [
130     (lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
131     lib.concatLists
132   ];
134   makePasswordFile = listenerScope: users: path:
135     let
136       makeLines = store: file: let
137         scopedUsers = toScopedUsers listenerScope users;
138       in
139         lib.mapAttrsToList
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";
147     in
148       pkgs.writeScript "make-mosquitto-passwd"
149         (''
150           #! ${pkgs.runtimeShell}
152           set -eu
154           file=${lib.escapeShellArg path}
156           rm -f "$file"
157           touch "$file"
159           addLine() {
160             echo "$1:$2" >> "$file"
161           }
162           addFile() {
163             if [ $(wc -l <"$2") -gt 1 ]; then
164               echo "invalid mosquitto password file $2" >&2
165               return 1
166             fi
167             echo "$1:$(cat "$2")" >> "$file"
168           }
169         ''
170         + lib.concatStringsSep "\n"
171           (plainLines
172            ++ lib.optional (plainLines != []) ''
173              ${cfg.package}/bin/mosquitto_passwd -U "$file"
174            ''
175            ++ hashedLines));
177   authPluginOptions = with lib.types; submodule {
178     options = {
179       plugin = lib.mkOption {
180         type = path;
181         description = ''
182           Plugin path to load, should be a `.so` file.
183         '';
184       };
186       denySpecialChars = lib.mkOption {
187         type = bool;
188         description = ''
189           Automatically disallow all clients using `#`
190           or `+` in their name/id.
191         '';
192         default = true;
193       };
195       options = lib.mkOption {
196         type = attrsOf optionType;
197         description = ''
198           Options for the auth plugin. Each key turns into a `auth_opt_*`
199            line in the config.
200         '';
201         default = {};
202       };
203     };
204   };
206   authAsserts = prefix: auth:
207     lib.mapAttrsToList
208       (n: _: {
209         assertion = configKey.check n;
210         message = "Invalid auth plugin key ${prefix}.${n}";
211       })
212       auth;
214   formatAuthPlugin = plugin:
215     [
216       "auth_plugin ${plugin.plugin}"
217       "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
218     ]
219     ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
221   freeformListenerKeys = {
222     allow_anonymous = 1;
223     allow_zero_length_clientid = 1;
224     auto_id_prefix = 1;
225     bind_interface = 1;
226     cafile = 1;
227     capath = 1;
228     certfile = 1;
229     ciphers = 1;
230     "ciphers_tls1.3" = 1;
231     crlfile = 1;
232     dhparamfile = 1;
233     http_dir = 1;
234     keyfile = 1;
235     max_connections = 1;
236     max_qos = 1;
237     max_topic_alias = 1;
238     mount_point = 1;
239     protocol = 1;
240     psk_file = 1;
241     psk_hint = 1;
242     require_certificate = 1;
243     socket_domain = 1;
244     tls_engine = 1;
245     tls_engine_kpass_sha1 = 1;
246     tls_keyform = 1;
247     tls_version = 1;
248     use_identity_as_username = 1;
249     use_subject_as_username = 1;
250     use_username_as_clientid = 1;
251   };
253   listenerOptions = with lib.types; submodule {
254     options = {
255       port = lib.mkOption {
256         type = port;
257         description = ''
258           Port to listen on. Must be set to 0 to listen on a unix domain socket.
259         '';
260         default = 1883;
261       };
263       address = lib.mkOption {
264         type = nullOr str;
265         description = ''
266           Address to listen on. Listen on `0.0.0.0`/`::`
267           when unset.
268         '';
269         default = null;
270       };
272       authPlugins = lib.mkOption {
273         type = listOf authPluginOptions;
274         description = ''
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.
278         '';
279         default = [];
280       };
282       users = lib.mkOption {
283         type = attrsOf userOptions;
284         example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
285         description = ''
286           A set of users and their passwords and ACLs.
287         '';
288         default = {};
289       };
291       omitPasswordAuth = lib.mkOption {
292         type = bool;
293         description = ''
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.
296         '';
297         default = false;
298       };
300       acl = lib.mkOption {
301         type = listOf str;
302         description = ''
303           Additional ACL items to prepend to the generated ACL file.
304         '';
305         example = [ "pattern read #" "topic readwrite anon/report/#" ];
306         default = [];
307       };
309       settings = lib.mkOption {
310         type = submodule {
311           freeformType = attrsOf optionType;
312         };
313         description = ''
314           Additional settings for this listener.
315         '';
316         default = {};
317       };
318     };
319   };
321   listenerAsserts = prefix: listener:
322     assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
323     ++ userAsserts prefix listener.users
324     ++ lib.imap0
325       (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
326       listener.authPlugins;
328   formatListener = idx: listener:
329     [
330       "listener ${toString listener.port} ${toString listener.address}"
331       "acl_file /etc/mosquitto/acl-${toString idx}.conf"
332     ]
333     ++ lib.optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
334     ++ formatFreeform {} listener.settings
335     ++ lib.concatMap formatAuthPlugin listener.authPlugins;
337   freeformBridgeKeys = {
338     bridge_alpn = 1;
339     bridge_attempt_unsubscribe = 1;
340     bridge_bind_address = 1;
341     bridge_cafile = 1;
342     bridge_capath = 1;
343     bridge_certfile = 1;
344     bridge_identity = 1;
345     bridge_insecure = 1;
346     bridge_keyfile = 1;
347     bridge_max_packet_size = 1;
348     bridge_outgoing_retain = 1;
349     bridge_protocol_version = 1;
350     bridge_psk = 1;
351     bridge_require_ocsp = 1;
352     bridge_tls_version = 1;
353     cleansession = 1;
354     idle_timeout = 1;
355     keepalive_interval = 1;
356     local_cleansession = 1;
357     local_clientid = 1;
358     local_password = 1;
359     local_username = 1;
360     notification_topic = 1;
361     notifications = 1;
362     notifications_local_only = 1;
363     remote_clientid = 1;
364     remote_password = 1;
365     remote_username = 1;
366     restart_timeout = 1;
367     round_robin = 1;
368     start_type = 1;
369     threshold = 1;
370     try_private = 1;
371   };
373   bridgeOptions = with lib.types; submodule {
374     options = {
375       addresses = lib.mkOption {
376         type = listOf (submodule {
377           options = {
378             address = lib.mkOption {
379               type = str;
380               description = ''
381                 Address of the remote MQTT broker.
382               '';
383             };
385             port = lib.mkOption {
386               type = port;
387               description = ''
388                 Port of the remote MQTT broker.
389               '';
390               default = 1883;
391             };
392           };
393         });
394         default = [];
395         description = ''
396           Remote endpoints for the bridge.
397         '';
398       };
400       topics = lib.mkOption {
401         type = listOf str;
402         description = ''
403           Topic patterns to be shared between the two brokers.
404           Refer to the [
405           mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
406         '';
407         default = [];
408         example = [ "# both 2 local/topic/ remote/topic/" ];
409       };
411       settings = lib.mkOption {
412         type = submodule {
413           freeformType = attrsOf optionType;
414         };
415         description = ''
416           Additional settings for this bridge.
417         '';
418         default = {};
419       };
420     };
421   };
423   bridgeAsserts = prefix: bridge:
424     assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
425     ++ [ {
426       assertion = lib.length bridge.addresses > 0;
427       message = "Bridge ${prefix} needs remote broker addresses";
428     } ];
430   formatBridge = name: bridge:
431     [
432       "connection ${name}"
433       "addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
434     ]
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;
444     log_facility = 1;
445     log_timestamp = 1;
446     log_timestamp_format = 1;
447     max_inflight_bytes = 1;
448     max_inflight_messages = 1;
449     max_keepalive = 1;
450     max_packet_size = 1;
451     max_queued_bytes = 1;
452     max_queued_messages = 1;
453     memory_limit = 1;
454     message_size_limit = 1;
455     persistence_file = 1;
456     persistence_location = 1;
457     persistent_client_expiration = 1;
458     pid_file = 1;
459     queue_qos0_messages = 1;
460     retain_available = 1;
461     set_tcp_nodelay = 1;
462     sys_interval = 1;
463     upgrade_outgoing_qos = 1;
464     websockets_headers_size = 1;
465     websockets_log_level = 1;
466   };
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;
475       default = {};
476       description = ''
477         Bridges to build to other MQTT brokers.
478       '';
479     };
481     listeners = lib.mkOption {
482       type = listOf listenerOptions;
483       default = [];
484       description = ''
485         Listeners to configure on this broker.
486       '';
487     };
489     includeDirs = lib.mkOption {
490       type = listOf path;
491       description = ''
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.
496       '';
497       default = [];
498     };
500     logDest = lib.mkOption {
501       type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
502       description = ''
503         Destinations to send log messages to.
504       '';
505       default = [ "stderr" ];
506     };
508     logType = lib.mkOption {
509       type = listOf (enum [ "debug" "error" "warning" "notice" "information"
510                             "subscribe" "unsubscribe" "websockets" "none" "all" ]);
511       description = ''
512         Types of messages to log.
513       '';
514       default = [];
515     };
517     persistence = lib.mkOption {
518       type = bool;
519       description = ''
520         Enable persistent storage of subscriptions and messages.
521       '';
522       default = true;
523     };
525     dataDir = lib.mkOption {
526       default = "/var/lib/mosquitto";
527       type = lib.types.path;
528       description = ''
529         The data directory.
530       '';
531     };
533     settings = lib.mkOption {
534       type = submodule {
535         freeformType = attrsOf optionType;
536       };
537       description = ''
538         Global configuration options for the mosquitto broker.
539       '';
540       default = {};
541     };
542   };
544   globalAsserts = prefix: cfg:
545     lib.flatten [
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)
549     ];
551   formatGlobal = cfg:
552     [
553       "per_listener_settings true"
554       "persistence ${optionToString cfg.persistence}"
555     ]
556     ++ map
557       (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
558       cfg.logDest
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));
572   ###### Interface
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" ];
587       serviceConfig = {
588         Type = "notify";
589         NotifyAccess = "main";
590         User = "mosquitto";
591         Group = "mosquitto";
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";
598         # Credentials
599         SetCredential = let
600           listenerCredentials = listenerScope: listener:
601             usersCredentials listenerScope listener.users [ "password" "hashedPassword" ];
602         in
603           systemdCredentials cfg.listeners listenerCredentials;
605         LoadCredential = let
606           listenerCredentials = listenerScope: listener:
607             usersCredentials listenerScope listener.users [ "passwordFile" "hashedPasswordFile" ];
608         in
609           systemdCredentials cfg.listeners listenerCredentials;
611         # Hardening
612         CapabilityBoundingSet = "";
613         DevicePolicy = "closed";
614         LockPersonality = true;
615         MemoryDenyWriteExecute = true;
616         NoNewPrivileges = true;
617         PrivateDevices = true;
618         PrivateTmp = true;
619         PrivateUsers = true;
620         ProtectClock = true;
621         ProtectControlGroups = true;
622         ProtectHome = true;
623         ProtectHostname = true;
624         ProtectKernelLogs = true;
625         ProtectKernelModules = true;
626         ProtectKernelTunables = true;
627         ProtectProc = "invisible";
628         ProcSubset = "pid";
629         ProtectSystem = "strict";
630         ReadWritePaths = [
631           cfg.dataDir
632           "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
633         ] ++ lib.filter path.check cfg.logDest;
634         ReadOnlyPaths =
635           map (p: "${p}")
636             (cfg.includeDirs
637              ++ lib.filter
638                (v: v != null)
639                (lib.flatten [
640                  (map
641                    (l: [
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)
650                    ])
651                    cfg.listeners)
652                  (lib.mapAttrsToList
653                    (_: b: [
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)
658                    ])
659                    cfg.bridges)
660                ]));
661         RemoveIPC = true;
662         RestrictAddressFamilies = [
663           "AF_UNIX"
664           "AF_INET"
665           "AF_INET6"
666           "AF_NETLINK"
667         ];
668         RestrictNamespaces = true;
669         RestrictRealtime = true;
670         RestrictSUIDSGID = true;
671         SystemCallArchitectures = "native";
672         SystemCallFilter = [
673           "@system-service"
674           "~@privileged"
675           "~@resources"
676         ];
677         UMask = "0077";
678       };
679       preStart =
680         lib.concatStringsSep
681           "\n"
682           (lib.imap0
683             (idx: listener: makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}")
684             cfg.listeners);
685     };
687     environment.etc = lib.listToAttrs (
688       lib.imap0
689         (idx: listener: {
690           name = "mosquitto/acl-${toString idx}.conf";
691           value = {
692             user = config.users.users.mosquitto.name;
693             group = config.users.users.mosquitto.group;
694             mode = "0400";
695             text = (lib.concatStringsSep
696               "\n"
697               (lib.flatten [
698                 listener.acl
699                 (lib.mapAttrsToList
700                   (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
701                   listener.users)
702               ]));
703           };
704         })
705         cfg.listeners
706     );
708     users.users.mosquitto = {
709       description = "Mosquitto MQTT Broker Daemon owner";
710       group = "mosquitto";
711       uid = config.ids.uids.mosquitto;
712       home = cfg.dataDir;
713       createHome = true;
714     };
716     users.groups.mosquitto.gid = config.ids.gids.mosquitto;
718   };
720   meta = {
721     maintainers = [ ];
722     doc = ./mosquitto.md;
723   };