python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / networking / mosquitto.nix
blob6543eb34b4b269f2debae785e81519d70bc30654
1 { config, lib, pkgs, ...}:
3 with lib;
5 let
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";
13   };
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";
18   };
19   optionToString = v:
20     if isBool v then boolToString v
21     else if path.check v then "${v}"
22     else toString v;
24   assertKeysValid = prefix: valid: config:
25     mapAttrsToList
26       (n: _: {
27         assertion = valid ? ${n};
28         message = "Invalid config key ${prefix}.${n}.";
29       })
30       config;
32   formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
34   userOptions = with types; submodule {
35     options = {
36       password = mkOption {
37         type = uniq (nullOr str);
38         default = null;
39         description = lib.mdDoc ''
40           Specifies the (clear text) password for the MQTT User.
41         '';
42       };
44       passwordFile = mkOption {
45         type = uniq (nullOr types.path);
46         example = "/path/to/file";
47         default = null;
48         description = lib.mdDoc ''
49           Specifies the path to a file containing the
50           clear text password for the MQTT user.
51         '';
52       };
54       hashedPassword = mkOption {
55         type = uniq (nullOr str);
56         default = null;
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
62           file.
63         '';
64       };
66       hashedPasswordFile = mkOption {
67         type = uniq (nullOr types.path);
68         example = "/path/to/file";
69         default = null;
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.
76         '';
77       };
79       acl = mkOption {
80         type = listOf str;
81         example = [ "read A/B" "readwrite A/#" ];
82         default = [];
83         description = lib.mdDoc ''
84           Control client access to topics on the broker.
85         '';
86       };
87     };
88   };
90   userAsserts = prefix: users:
91     mapAttrsToList
92       (n: _: {
93         assertion = builtins.match "[^:\r\n]+" n != null;
94         message = "Invalid user name ${n} in ${prefix}";
95       })
96       users
97     ++ mapAttrsToList
98       (n: u: {
99         assertion = count (s: s != null) [
100           u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
101         ] <= 1;
102         message = "Cannot set more than one password option for user ${n} in ${prefix}";
103       }) users;
105   makePasswordFile = users: path:
106     let
107       makeLines = store: file:
108         mapAttrsToList
109           (n: u: "addLine ${escapeShellArg n} ${escapeShellArg u.${store}}")
110           (filterAttrs (_: u: u.${store} != null) users)
111         ++ mapAttrsToList
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";
116     in
117       pkgs.writeScript "make-mosquitto-passwd"
118         (''
119           #! ${pkgs.runtimeShell}
121           set -eu
123           file=${escapeShellArg path}
125           rm -f "$file"
126           touch "$file"
128           addLine() {
129             echo "$1:$2" >> "$file"
130           }
131           addFile() {
132             if [ $(wc -l <"$2") -gt 1 ]; then
133               echo "invalid mosquitto password file $2" >&2
134               return 1
135             fi
136             echo "$1:$(cat "$2")" >> "$file"
137           }
138         ''
139         + concatStringsSep "\n"
140           (plainLines
141            ++ optional (plainLines != []) ''
142              ${cfg.package}/bin/mosquitto_passwd -U "$file"
143            ''
144            ++ hashedLines));
146   makeACLFile = idx: users: supplement:
147     pkgs.writeText "mosquitto-acl-${toString idx}.conf"
148       (concatStringsSep
149         "\n"
150         (flatten [
151           supplement
152           (mapAttrsToList
153             (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
154             users)
155         ]));
157   authPluginOptions = with types; submodule {
158     options = {
159       plugin = mkOption {
160         type = path;
161         description = mdDoc ''
162           Plugin path to load, should be a `.so` file.
163         '';
164       };
166       denySpecialChars = mkOption {
167         type = bool;
168         description = mdDoc ''
169           Automatically disallow all clients using `#`
170           or `+` in their name/id.
171         '';
172         default = true;
173       };
175       options = mkOption {
176         type = attrsOf optionType;
177         description = mdDoc ''
178           Options for the auth plugin. Each key turns into a `auth_opt_*`
179            line in the config.
180         '';
181         default = {};
182       };
183     };
184   };
186   authAsserts = prefix: auth:
187     mapAttrsToList
188       (n: _: {
189         assertion = configKey.check n;
190         message = "Invalid auth plugin key ${prefix}.${n}";
191       })
192       auth;
194   formatAuthPlugin = plugin:
195     [
196       "auth_plugin ${plugin.plugin}"
197       "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
198     ]
199     ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
201   freeformListenerKeys = {
202     allow_anonymous = 1;
203     allow_zero_length_clientid = 1;
204     auto_id_prefix = 1;
205     bind_interface = 1;
206     cafile = 1;
207     capath = 1;
208     certfile = 1;
209     ciphers = 1;
210     "ciphers_tls1.3" = 1;
211     crlfile = 1;
212     dhparamfile = 1;
213     http_dir = 1;
214     keyfile = 1;
215     max_connections = 1;
216     max_qos = 1;
217     max_topic_alias = 1;
218     mount_point = 1;
219     protocol = 1;
220     psk_file = 1;
221     psk_hint = 1;
222     require_certificate = 1;
223     socket_domain = 1;
224     tls_engine = 1;
225     tls_engine_kpass_sha1 = 1;
226     tls_keyform = 1;
227     tls_version = 1;
228     use_identity_as_username = 1;
229     use_subject_as_username = 1;
230     use_username_as_clientid = 1;
231   };
233   listenerOptions = with types; submodule {
234     options = {
235       port = mkOption {
236         type = port;
237         description = lib.mdDoc ''
238           Port to listen on. Must be set to 0 to listen on a unix domain socket.
239         '';
240         default = 1883;
241       };
243       address = mkOption {
244         type = nullOr str;
245         description = mdDoc ''
246           Address to listen on. Listen on `0.0.0.0`/`::`
247           when unset.
248         '';
249         default = null;
250       };
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.
258         '';
259         default = [];
260       };
262       users = mkOption {
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.
267         '';
268         default = {};
269       };
271       omitPasswordAuth = mkOption {
272         type = bool;
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.
276         '';
277         default = false;
278       };
280       acl = mkOption {
281         type = listOf str;
282         description = lib.mdDoc ''
283           Additional ACL items to prepend to the generated ACL file.
284         '';
285         example = [ "pattern read #" "topic readwrite anon/report/#" ];
286         default = [];
287       };
289       settings = mkOption {
290         type = submodule {
291           freeformType = attrsOf optionType;
292         };
293         description = lib.mdDoc ''
294           Additional settings for this listener.
295         '';
296         default = {};
297       };
298     };
299   };
301   listenerAsserts = prefix: listener:
302     assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
303     ++ userAsserts prefix listener.users
304     ++ imap0
305       (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
306       listener.authPlugins;
308   formatListener = idx: listener:
309     [
310       "listener ${toString listener.port} ${toString listener.address}"
311       "acl_file ${makeACLFile idx listener.users listener.acl}"
312     ]
313     ++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
314     ++ formatFreeform {} listener.settings
315     ++ concatMap formatAuthPlugin listener.authPlugins;
317   freeformBridgeKeys = {
318     bridge_alpn = 1;
319     bridge_attempt_unsubscribe = 1;
320     bridge_bind_address = 1;
321     bridge_cafile = 1;
322     bridge_capath = 1;
323     bridge_certfile = 1;
324     bridge_identity = 1;
325     bridge_insecure = 1;
326     bridge_keyfile = 1;
327     bridge_max_packet_size = 1;
328     bridge_outgoing_retain = 1;
329     bridge_protocol_version = 1;
330     bridge_psk = 1;
331     bridge_require_ocsp = 1;
332     bridge_tls_version = 1;
333     cleansession = 1;
334     idle_timeout = 1;
335     keepalive_interval = 1;
336     local_cleansession = 1;
337     local_clientid = 1;
338     local_password = 1;
339     local_username = 1;
340     notification_topic = 1;
341     notifications = 1;
342     notifications_local_only = 1;
343     remote_clientid = 1;
344     remote_password = 1;
345     remote_username = 1;
346     restart_timeout = 1;
347     round_robin = 1;
348     start_type = 1;
349     threshold = 1;
350     try_private = 1;
351   };
353   bridgeOptions = with types; submodule {
354     options = {
355       addresses = mkOption {
356         type = listOf (submodule {
357           options = {
358             address = mkOption {
359               type = str;
360               description = lib.mdDoc ''
361                 Address of the remote MQTT broker.
362               '';
363             };
365             port = mkOption {
366               type = port;
367               description = lib.mdDoc ''
368                 Port of the remote MQTT broker.
369               '';
370               default = 1883;
371             };
372           };
373         });
374         default = [];
375         description = lib.mdDoc ''
376           Remote endpoints for the bridge.
377         '';
378       };
380       topics = mkOption {
381         type = listOf str;
382         description = lib.mdDoc ''
383           Topic patterns to be shared between the two brokers.
384           Refer to the [
385           mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html) for details on the format.
386         '';
387         default = [];
388         example = [ "# both 2 local/topic/ remote/topic/" ];
389       };
391       settings = mkOption {
392         type = submodule {
393           freeformType = attrsOf optionType;
394         };
395         description = lib.mdDoc ''
396           Additional settings for this bridge.
397         '';
398         default = {};
399       };
400     };
401   };
403   bridgeAsserts = prefix: bridge:
404     assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
405     ++ [ {
406       assertion = length bridge.addresses > 0;
407       message = "Bridge ${prefix} needs remote broker addresses";
408     } ];
410   formatBridge = name: bridge:
411     [
412       "connection ${name}"
413       "addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
414     ]
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;
424     log_facility = 1;
425     log_timestamp = 1;
426     log_timestamp_format = 1;
427     max_inflight_bytes = 1;
428     max_inflight_messages = 1;
429     max_keepalive = 1;
430     max_packet_size = 1;
431     max_queued_bytes = 1;
432     max_queued_messages = 1;
433     memory_limit = 1;
434     message_size_limit = 1;
435     persistence_file = 1;
436     persistence_location = 1;
437     persistent_client_expiration = 1;
438     pid_file = 1;
439     queue_qos0_messages = 1;
440     retain_available = 1;
441     set_tcp_nodelay = 1;
442     sys_interval = 1;
443     upgrade_outgoing_qos = 1;
444     websockets_headers_size = 1;
445     websockets_log_level = 1;
446   };
448   globalOptions = with types; {
449     enable = mkEnableOption (lib.mdDoc "the MQTT Mosquitto broker");
451     package = mkOption {
452       type = package;
453       default = pkgs.mosquitto;
454       defaultText = literalExpression "pkgs.mosquitto";
455       description = lib.mdDoc ''
456         Mosquitto package to use.
457       '';
458     };
460     bridges = mkOption {
461       type = attrsOf bridgeOptions;
462       default = {};
463       description = lib.mdDoc ''
464         Bridges to build to other MQTT brokers.
465       '';
466     };
468     listeners = mkOption {
469       type = listOf listenerOptions;
470       default = {};
471       description = lib.mdDoc ''
472         Listeners to configure on this broker.
473       '';
474     };
476     includeDirs = mkOption {
477       type = listOf path;
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.
483       '';
484       default = [];
485     };
487     logDest = mkOption {
488       type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
489       description = lib.mdDoc ''
490         Destinations to send log messages to.
491       '';
492       default = [ "stderr" ];
493     };
495     logType = mkOption {
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.
500       '';
501       default = [];
502     };
504     persistence = mkOption {
505       type = bool;
506       description = lib.mdDoc ''
507         Enable persistent storage of subscriptions and messages.
508       '';
509       default = true;
510     };
512     dataDir = mkOption {
513       default = "/var/lib/mosquitto";
514       type = types.path;
515       description = lib.mdDoc ''
516         The data directory.
517       '';
518     };
520     settings = mkOption {
521       type = submodule {
522         freeformType = attrsOf optionType;
523       };
524       description = lib.mdDoc ''
525         Global configuration options for the mosquitto broker.
526       '';
527       default = {};
528     };
529   };
531   globalAsserts = prefix: cfg:
532     flatten [
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)
536     ];
538   formatGlobal = cfg:
539     [
540       "per_listener_settings true"
541       "persistence ${optionToString cfg.persistence}"
542     ]
543     ++ map
544       (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
545       cfg.logDest
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));
559   ###### Interface
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" ];
573       serviceConfig = {
574         Type = "notify";
575         NotifyAccess = "main";
576         User = "mosquitto";
577         Group = "mosquitto";
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";
584         # Hardening
585         CapabilityBoundingSet = "";
586         DevicePolicy = "closed";
587         LockPersonality = true;
588         MemoryDenyWriteExecute = true;
589         NoNewPrivileges = true;
590         PrivateDevices = true;
591         PrivateTmp = true;
592         PrivateUsers = true;
593         ProtectClock = true;
594         ProtectControlGroups = true;
595         ProtectHome = true;
596         ProtectHostname = true;
597         ProtectKernelLogs = true;
598         ProtectKernelModules = true;
599         ProtectKernelTunables = true;
600         ProtectProc = "invisible";
601         ProcSubset = "pid";
602         ProtectSystem = "strict";
603         ReadWritePaths = [
604           cfg.dataDir
605           "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
606         ] ++ filter path.check cfg.logDest;
607         ReadOnlyPaths =
608           map (p: "${p}")
609             (cfg.includeDirs
610              ++ filter
611                (v: v != null)
612                (flatten [
613                  (map
614                    (l: [
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)
623                    ])
624                    cfg.listeners)
625                  (mapAttrsToList
626                    (_: b: [
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)
631                    ])
632                    cfg.bridges)
633                ]));
634         RemoveIPC = true;
635         RestrictAddressFamilies = [
636           "AF_UNIX"
637           "AF_INET"
638           "AF_INET6"
639           "AF_NETLINK"
640         ];
641         RestrictNamespaces = true;
642         RestrictRealtime = true;
643         RestrictSUIDSGID = true;
644         SystemCallArchitectures = "native";
645         SystemCallFilter = [
646           "@system-service"
647           "~@privileged"
648           "~@resources"
649         ];
650         UMask = "0077";
651       };
652       preStart =
653         concatStringsSep
654           "\n"
655           (imap0
656             (idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}")
657             cfg.listeners);
658     };
660     users.users.mosquitto = {
661       description = "Mosquitto MQTT Broker Daemon owner";
662       group = "mosquitto";
663       uid = config.ids.uids.mosquitto;
664       home = cfg.dataDir;
665       createHome = true;
666     };
668     users.groups.mosquitto.gid = config.ids.gids.mosquitto;
670   };
672   meta = {
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;
677   };