nixos/mosquitto: rewrite the module

mosquitto needs a lot of attention concerning its config because it doesn't
parse it very well, often ignoring trailing parts of lines, duplicated config
keys, or just looking back way further in the file to associated config keys
with previously defined items than might be expected.

this replaces the mosquitto module completely. we now have a hierarchical config
that flattens out to the mosquitto format (hopefully) without introducing spooky
action at a distance.

authored by

pennae and committed by
tomberek
56d0b5cd 4b91c770

+540 -182
+8
nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
··· 1138 <literal>coursier</literal>, you can create a shell alias. 1139 </para> 1140 </listitem> 1141 </itemizedlist> 1142 </section> 1143 <section xml:id="sec-release-21.11-notable-changes">
··· 1138 <literal>coursier</literal>, you can create a shell alias. 1139 </para> 1140 </listitem> 1141 + <listitem> 1142 + <para> 1143 + The <literal>services.mosquitto</literal> module has been 1144 + rewritten to support multiple listeners and per-listener 1145 + configuration. Module configurations from previous releases 1146 + will no longer work and must be updated. 1147 + </para> 1148 + </listitem> 1149 </itemizedlist> 1150 </section> 1151 <section xml:id="sec-release-21.11-notable-changes">
+3
nixos/doc/manual/release-notes/rl-2111.section.md
··· 351 352 - The `coursier` package's binary was renamed from `coursier` to `cs`. Completions which haven't worked for a while should now work with the renamed binary. To keep using `coursier`, you can create a shell alias. 353 354 ## Other Notable Changes {#sec-release-21.11-notable-changes} 355 356
··· 351 352 - The `coursier` package's binary was renamed from `coursier` to `cs`. Completions which haven't worked for a while should now work with the renamed binary. To keep using `coursier`, you can create a shell alias. 353 354 + - The `services.mosquitto` module has been rewritten to support multiple listeners and per-listener configuration. 355 + Module configurations from previous releases will no longer work and must be updated. 356 + 357 ## Other Notable Changes {#sec-release-21.11-notable-changes} 358 359
+511 -167
nixos/modules/services/networking/mosquitto.nix
··· 5 let 6 cfg = config.services.mosquitto; 7 8 - listenerConf = optionalString cfg.ssl.enable '' 9 - listener ${toString cfg.ssl.port} ${cfg.ssl.host} 10 - cafile ${cfg.ssl.cafile} 11 - certfile ${cfg.ssl.certfile} 12 - keyfile ${cfg.ssl.keyfile} 13 - ''; 14 15 - passwordConf = optionalString cfg.checkPasswords '' 16 - password_file ${cfg.dataDir}/passwd 17 - ''; 18 19 - mosquittoConf = pkgs.writeText "mosquitto.conf" '' 20 - acl_file ${aclFile} 21 - persistence true 22 - allow_anonymous ${boolToString cfg.allowAnonymous} 23 - listener ${toString cfg.port} ${cfg.host} 24 - ${passwordConf} 25 - ${listenerConf} 26 - ${cfg.extraConf} 27 - ''; 28 29 - userAcl = (concatStringsSep "\n\n" (mapAttrsToList (n: c: 30 - "user ${n}\n" + (concatStringsSep "\n" c.acl)) cfg.users 31 - )); 32 33 - aclFile = pkgs.writeText "mosquitto.acl" '' 34 - ${cfg.aclExtraConf} 35 - ${userAcl} 36 - ''; 37 38 - in 39 40 - { 41 42 - ###### Interface 43 44 - options = { 45 - services.mosquitto = { 46 - enable = mkEnableOption "the MQTT Mosquitto broker"; 47 48 - host = mkOption { 49 - default = "127.0.0.1"; 50 - example = "0.0.0.0"; 51 - type = types.str; 52 description = '' 53 - Host to listen on without SSL. 54 ''; 55 }; 56 57 - port = mkOption { 58 - default = 1883; 59 - type = types.int; 60 description = '' 61 - Port on which to listen without SSL. 62 ''; 63 }; 64 65 - ssl = { 66 - enable = mkEnableOption "SSL listener"; 67 68 - cafile = mkOption { 69 - type = types.nullOr types.path; 70 - default = null; 71 - description = "Path to PEM encoded CA certificates."; 72 - }; 73 74 - certfile = mkOption { 75 - type = types.nullOr types.path; 76 - default = null; 77 - description = "Path to PEM encoded server certificate."; 78 - }; 79 80 - keyfile = mkOption { 81 - type = types.nullOr types.path; 82 - default = null; 83 - description = "Path to PEM encoded server key."; 84 - }; 85 86 - host = mkOption { 87 - default = "0.0.0.0"; 88 - example = "localhost"; 89 - type = types.str; 90 - description = '' 91 - Host to listen on with SSL. 92 - ''; 93 - }; 94 95 - port = mkOption { 96 - default = 8883; 97 - type = types.int; 98 - description = '' 99 - Port on which to listen with SSL. 100 - ''; 101 - }; 102 }; 103 104 - dataDir = mkOption { 105 - default = "/var/lib/mosquitto"; 106 - type = types.path; 107 description = '' 108 - The data directory. 109 ''; 110 }; 111 112 users = mkOption { 113 - type = types.attrsOf (types.submodule { 114 - options = { 115 - password = mkOption { 116 - type = with types; uniq (nullOr str); 117 - default = null; 118 - description = '' 119 - Specifies the (clear text) password for the MQTT User. 120 - ''; 121 - }; 122 123 - passwordFile = mkOption { 124 - type = with types; uniq (nullOr str); 125 - example = "/path/to/file"; 126 - default = null; 127 - description = '' 128 - Specifies the path to a file containing the 129 - clear text password for the MQTT user. 130 - ''; 131 - }; 132 133 - hashedPassword = mkOption { 134 - type = with types; uniq (nullOr str); 135 - default = null; 136 - description = '' 137 - Specifies the hashed password for the MQTT User. 138 - To generate hashed password install <literal>mosquitto</literal> 139 - package and use <literal>mosquitto_passwd</literal>. 140 - ''; 141 - }; 142 143 - hashedPasswordFile = mkOption { 144 - type = with types; uniq (nullOr str); 145 - example = "/path/to/file"; 146 - default = null; 147 description = '' 148 - Specifies the path to a file containing the 149 - hashed password for the MQTT user. 150 - To generate hashed password install <literal>mosquitto</literal> 151 - package and use <literal>mosquitto_passwd</literal>. 152 ''; 153 }; 154 155 - acl = mkOption { 156 - type = types.listOf types.str; 157 - example = [ "topic read A/B" "topic A/#" ]; 158 description = '' 159 - Control client access to topics on the broker. 160 ''; 161 }; 162 }; 163 }); 164 - example = { john = { password = "123456"; acl = [ "topic readwrite john/#" ]; }; }; 165 description = '' 166 - A set of users and their passwords and ACLs. 167 ''; 168 }; 169 170 - allowAnonymous = mkOption { 171 - default = false; 172 - type = types.bool; 173 description = '' 174 - Allow clients to connect without authentication. 175 ''; 176 }; 177 178 - checkPasswords = mkOption { 179 - default = false; 180 - example = true; 181 - type = types.bool; 182 description = '' 183 - Refuse connection when clients provide incorrect passwords. 184 ''; 185 }; 186 187 - extraConf = mkOption { 188 - default = ""; 189 - type = types.lines; 190 - description = '' 191 - Extra config to append to `mosquitto.conf` file. 192 - ''; 193 - }; 194 195 - aclExtraConf = mkOption { 196 - default = ""; 197 - type = types.lines; 198 - description = '' 199 - Extra config to prepend to the ACL file. 200 - ''; 201 - }; 202 203 }; 204 }; 205 206 207 ###### Implementation 208 209 config = mkIf cfg.enable { 210 211 - assertions = mapAttrsToList (name: cfg: { 212 - assertion = length (filter (s: s != null) (with cfg; [ 213 - password passwordFile hashedPassword hashedPasswordFile 214 - ])) <= 1; 215 - message = "Cannot set more than one password option"; 216 - }) cfg.users; 217 218 systemd.services.mosquitto = { 219 description = "Mosquitto MQTT Broker Daemon"; ··· 227 RuntimeDirectory = "mosquitto"; 228 WorkingDirectory = cfg.dataDir; 229 Restart = "on-failure"; 230 - ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}"; 231 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 232 233 # Hardening ··· 252 ReadWritePaths = [ 253 cfg.dataDir 254 "/tmp" # mosquitto_passwd creates files in /tmp before moving them 255 - ]; 256 - ReadOnlyPaths = with cfg.ssl; lib.optionals (enable) [ 257 - certfile 258 - keyfile 259 - cafile 260 - ]; 261 RemoveIPC = true; 262 RestrictAddressFamilies = [ 263 "AF_UNIX" # for sd_notify() call ··· 275 ]; 276 UMask = "0077"; 277 }; 278 - preStart = '' 279 - rm -f ${cfg.dataDir}/passwd 280 - touch ${cfg.dataDir}/passwd 281 - '' + concatStringsSep "\n" ( 282 - mapAttrsToList (n: c: 283 - if c.hashedPasswordFile != null then 284 - "echo '${n}:'$(cat '${c.hashedPasswordFile}') >> ${cfg.dataDir}/passwd" 285 - else if c.passwordFile != null then 286 - "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} $(cat '${c.passwordFile}')" 287 - else if c.hashedPassword != null then 288 - "echo '${n}:${c.hashedPassword}' >> ${cfg.dataDir}/passwd" 289 - else optionalString (c.password != null) 290 - "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} '${c.password}'" 291 - ) cfg.users); 292 }; 293 294 users.users.mosquitto = { ··· 302 users.groups.mosquitto.gid = config.ids.gids.mosquitto; 303 304 }; 305 }
··· 5 let 6 cfg = config.services.mosquitto; 7 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; 23 24 + assertKeysValid = prefix: valid: config: 25 + mapAttrsToList 26 + (n: _: { 27 + assertion = valid ? ${n}; 28 + message = "Invalid config key ${prefix}.${n}."; 29 + }) 30 + config; 31 32 + formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}"); 33 34 + userOptions = with types; submodule { 35 + options = { 36 + password = mkOption { 37 + type = uniq (nullOr str); 38 + default = null; 39 + description = '' 40 + Specifies the (clear text) password for the MQTT User. 41 + ''; 42 + }; 43 44 + passwordFile = mkOption { 45 + type = uniq (nullOr types.path); 46 + example = "/path/to/file"; 47 + default = null; 48 + description = '' 49 + Specifies the path to a file containing the 50 + clear text password for the MQTT user. 51 + ''; 52 + }; 53 54 + hashedPassword = 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 <literal>mosquitto</literal> 60 + package and use <literal>mosquitto_passwd</literal>. 61 + ''; 62 + }; 63 64 + hashedPasswordFile = mkOption { 65 + type = uniq (nullOr types.path); 66 + example = "/path/to/file"; 67 + default = null; 68 + description = '' 69 + Specifies the path to a file containing the 70 + hashed password for the MQTT user. 71 + To generate hashed password install <literal>mosquitto</literal> 72 + package and use <literal>mosquitto_passwd</literal>. 73 + ''; 74 + }; 75 76 + acl = mkOption { 77 + type = listOf str; 78 + example = [ "read A/B" "readwrite A/#" ]; 79 + default = []; 80 + description = '' 81 + Control client access to topics on the broker. 82 + ''; 83 + }; 84 + }; 85 + }; 86 + 87 + userAsserts = prefix: users: 88 + mapAttrsToList 89 + (n: _: { 90 + assertion = builtins.match "[^:\r\n]+" n != null; 91 + message = "Invalid user name ${n} in ${prefix}"; 92 + }) 93 + users 94 + ++ mapAttrsToList 95 + (n: u: { 96 + assertion = count (s: s != null) [ 97 + u.password u.passwordFile u.hashedPassword u.hashedPasswordFile 98 + ] <= 1; 99 + message = "Cannot set more than one password option for user ${n} in ${prefix}"; 100 + }) users; 101 + 102 + makePasswordFile = users: path: 103 + let 104 + makeLines = store: file: 105 + mapAttrsToList 106 + (n: u: "addLine ${escapeShellArg n} ${escapeShellArg u.${store}}") 107 + (filterAttrs (_: u: u.${store} != null) users) 108 + ++ mapAttrsToList 109 + (n: u: "addFile ${escapeShellArg n} ${escapeShellArg "${u.${file}}"}") 110 + (filterAttrs (_: u: u.${file} != null) users); 111 + plainLines = makeLines "password" "passwordFile"; 112 + hashedLines = makeLines "hashedPassword" "hashedPasswordFile"; 113 + in 114 + pkgs.writeScript "make-mosquitto-passwd" 115 + ('' 116 + #! ${pkgs.runtimeShell} 117 + 118 + set -eu 119 + 120 + file=${escapeShellArg path} 121 122 + rm -f "$file" 123 + touch "$file" 124 125 + addLine() { 126 + echo "$1:$2" >> "$file" 127 + } 128 + addFile() { 129 + if [ $(wc -l <"$2") -gt 1 ]; then 130 + echo "invalid mosquitto password file $2" >&2 131 + return 1 132 + fi 133 + echo "$1:$(cat "$2")" >> "$file" 134 + } 135 + '' 136 + + concatStringsSep "\n" 137 + (plainLines 138 + ++ optional (plainLines != []) '' 139 + ${pkgs.mosquitto}/bin/mosquitto_passwd -U "$file" 140 + '' 141 + ++ hashedLines)); 142 + 143 + makeACLFile = idx: users: supplement: 144 + pkgs.writeText "mosquitto-acl-${toString idx}.conf" 145 + (concatStringsSep 146 + "\n" 147 + (flatten [ 148 + supplement 149 + (mapAttrsToList 150 + (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl) 151 + users) 152 + ])); 153 + 154 + authPluginOptions = with types; submodule { 155 + options = { 156 + plugin = mkOption { 157 + type = path; 158 description = '' 159 + Plugin path to load, should be a <literal>.so</literal> file. 160 ''; 161 }; 162 163 + denySpecialChars = mkOption { 164 + type = bool; 165 description = '' 166 + Automatically disallow all clients using <literal>#</literal> 167 + or <literal>+</literal> in their name/id. 168 ''; 169 + default = true; 170 }; 171 172 + options = mkOption { 173 + type = attrsOf optionType; 174 + description = '' 175 + Options for the auth plugin. Each key turns into a <literal>auth_opt_*</literal> 176 + line in the config. 177 + ''; 178 + default = {}; 179 + }; 180 + }; 181 + }; 182 183 + authAsserts = prefix: auth: 184 + mapAttrsToList 185 + (n: _: { 186 + assertion = configKey.check n; 187 + message = "Invalid auth plugin key ${prefix}.${n}"; 188 + }) 189 + auth; 190 191 + formatAuthPlugin = plugin: 192 + [ 193 + "auth_plugin ${plugin.plugin}" 194 + "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}" 195 + ] 196 + ++ formatFreeform { prefix = "auth_opt_"; } plugin.options; 197 198 + freeformListenerKeys = { 199 + allow_anonymous = 1; 200 + allow_zero_length_clientid = 1; 201 + auto_id_prefix = 1; 202 + cafile = 1; 203 + capath = 1; 204 + certfile = 1; 205 + ciphers = 1; 206 + "ciphers_tls1.3" = 1; 207 + crlfile = 1; 208 + dhparamfile = 1; 209 + http_dir = 1; 210 + keyfile = 1; 211 + max_connections = 1; 212 + max_qos = 1; 213 + max_topic_alias = 1; 214 + mount_point = 1; 215 + protocol = 1; 216 + psk_file = 1; 217 + psk_hint = 1; 218 + require_certificate = 1; 219 + socket_domain = 1; 220 + tls_engine = 1; 221 + tls_engine_kpass_sha1 = 1; 222 + tls_keyform = 1; 223 + tls_version = 1; 224 + use_identity_as_username = 1; 225 + use_subject_as_username = 1; 226 + use_username_as_clientid = 1; 227 + }; 228 229 + listenerOptions = with types; submodule { 230 + options = { 231 + port = mkOption { 232 + type = port; 233 + description = '' 234 + Port to listen on. Must be set to 0 to listen on a unix domain socket. 235 + ''; 236 + default = 1883; 237 + }; 238 239 + address = mkOption { 240 + type = nullOr str; 241 + description = '' 242 + Address to listen on. Listen on <literal>0.0.0.0</literal>/<literal>::</literal> 243 + when unset. 244 + ''; 245 + default = null; 246 }; 247 248 + authPlugins = mkOption { 249 + type = listOf authPluginOptions; 250 description = '' 251 + Authentication plugin to attach to this listener. 252 + Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html"> 253 + mosquitto.conf documentation</link> for details on authentication plugins. 254 ''; 255 + default = []; 256 }; 257 258 users = mkOption { 259 + type = attrsOf userOptions; 260 + example = { john = { password = "123456"; acl = [ "topic readwrite john/#" ]; }; }; 261 + description = '' 262 + A set of users and their passwords and ACLs. 263 + ''; 264 + default = {}; 265 + }; 266 267 + acl = mkOption { 268 + type = listOf str; 269 + description = '' 270 + Additional ACL items to prepend to the generated ACL file. 271 + ''; 272 + default = []; 273 + }; 274 275 + settings = mkOption { 276 + type = submodule { 277 + freeformType = attrsOf optionType; 278 + }; 279 + description = '' 280 + Additional settings for this listener. 281 + ''; 282 + default = {}; 283 + }; 284 + }; 285 + }; 286 287 + listenerAsserts = prefix: listener: 288 + assertKeysValid prefix freeformListenerKeys listener.settings 289 + ++ userAsserts prefix listener.users 290 + ++ imap0 291 + (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v) 292 + listener.authPlugins; 293 + 294 + formatListener = idx: listener: 295 + [ 296 + "listener ${toString listener.port} ${toString listener.address}" 297 + "password_file ${cfg.dataDir}/passwd-${toString idx}" 298 + "acl_file ${makeACLFile idx listener.users listener.acl}" 299 + ] 300 + ++ formatFreeform {} listener.settings 301 + ++ concatMap formatAuthPlugin listener.authPlugins; 302 + 303 + freeformBridgeKeys = { 304 + bridge_alpn = 1; 305 + bridge_attempt_unsubscribe = 1; 306 + bridge_bind_address = 1; 307 + bridge_cafile = 1; 308 + bridge_capath = 1; 309 + bridge_certfile = 1; 310 + bridge_identity = 1; 311 + bridge_insecure = 1; 312 + bridge_keyfile = 1; 313 + bridge_max_packet_size = 1; 314 + bridge_outgoing_retain = 1; 315 + bridge_protocol_version = 1; 316 + bridge_psk = 1; 317 + bridge_require_ocsp = 1; 318 + bridge_tls_version = 1; 319 + cleansession = 1; 320 + idle_timeout = 1; 321 + keepalive_interval = 1; 322 + local_cleansession = 1; 323 + local_clientid = 1; 324 + local_password = 1; 325 + local_username = 1; 326 + notification_topic = 1; 327 + notifications = 1; 328 + notifications_local_only = 1; 329 + remote_clientid = 1; 330 + remote_password = 1; 331 + remote_username = 1; 332 + restart_timeout = 1; 333 + round_robin = 1; 334 + start_type = 1; 335 + threshold = 1; 336 + try_private = 1; 337 + }; 338 + 339 + bridgeOptions = with types; submodule { 340 + options = { 341 + addresses = mkOption { 342 + type = listOf (submodule { 343 + options = { 344 + address = mkOption { 345 + type = str; 346 description = '' 347 + Address of the remote MQTT broker. 348 ''; 349 }; 350 351 + port = mkOption { 352 + type = port; 353 description = '' 354 + Port of the remote MQTT broker. 355 ''; 356 + default = 1883; 357 }; 358 }; 359 }); 360 + default = []; 361 description = '' 362 + Remote endpoints for the bridge. 363 ''; 364 }; 365 366 + topics = mkOption { 367 + type = listOf str; 368 description = '' 369 + Topic patterns to be shared between the two brokers. 370 + Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html"> 371 + mosquitto.conf documentation</link> for details on the format. 372 ''; 373 + default = []; 374 + example = [ "# both 2 local/topic/ remote/topic/" ]; 375 }; 376 377 + settings = mkOption { 378 + type = submodule { 379 + freeformType = attrsOf optionType; 380 + }; 381 description = '' 382 + Additional settings for this bridge. 383 ''; 384 + default = {}; 385 }; 386 + }; 387 + }; 388 389 + bridgeAsserts = prefix: bridge: 390 + assertKeysValid prefix freeformBridgeKeys bridge.settings 391 + ++ [ { 392 + assertion = length bridge.addresses > 0; 393 + message = "Bridge ${prefix} needs remote broker addresses"; 394 + } ]; 395 + 396 + formatBridge = name: bridge: 397 + [ 398 + "connection ${name}" 399 + "addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}" 400 + ] 401 + ++ map (t: "topic ${t}") bridge.topics 402 + ++ formatFreeform {} bridge.settings; 403 + 404 + freeformGlobalKeys = { 405 + allow_duplicate_messages = 1; 406 + autosave_interval = 1; 407 + autosave_on_changes = 1; 408 + check_retain_source = 1; 409 + connection_messages = 1; 410 + log_facility = 1; 411 + log_timestamp = 1; 412 + log_timestamp_format = 1; 413 + max_inflight_bytes = 1; 414 + max_inflight_messages = 1; 415 + max_keepalive = 1; 416 + max_packet_size = 1; 417 + max_queued_bytes = 1; 418 + max_queued_messages = 1; 419 + memory_limit = 1; 420 + message_size_limit = 1; 421 + persistence_file = 1; 422 + persistence_location = 1; 423 + persistent_client_expiration = 1; 424 + pid_file = 1; 425 + queue_qos0_messages = 1; 426 + retain_available = 1; 427 + set_tcp_nodelay = 1; 428 + sys_interval = 1; 429 + upgrade_outgoing_qos = 1; 430 + websockets_headers_size = 1; 431 + websockets_log_level = 1; 432 + }; 433 + 434 + globalOptions = with types; { 435 + enable = mkEnableOption "the MQTT Mosquitto broker"; 436 + 437 + bridges = mkOption { 438 + type = attrsOf bridgeOptions; 439 + default = {}; 440 + description = '' 441 + Bridges to build to other MQTT brokers. 442 + ''; 443 + }; 444 + 445 + listeners = mkOption { 446 + type = listOf listenerOptions; 447 + default = {}; 448 + description = '' 449 + Listeners to configure on this broker. 450 + ''; 451 + }; 452 + 453 + includeDirs = mkOption { 454 + type = listOf path; 455 + description = '' 456 + Directories to be scanned for further config files to include. 457 + Directories will processed in the order given, 458 + <literal>*.conf</literal> files in the directory will be 459 + read in case-sensistive alphabetical order. 460 + ''; 461 + default = []; 462 + }; 463 + 464 + logDest = mkOption { 465 + type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ])); 466 + description = '' 467 + Destinations to send log messages to. 468 + ''; 469 + default = [ "stderr" ]; 470 + }; 471 + 472 + logType = mkOption { 473 + type = listOf (enum [ "debug" "error" "warning" "notice" "information" 474 + "subscribe" "unsubscribe" "websockets" "none" "all" ]); 475 + description = '' 476 + Types of messages to log. 477 + ''; 478 + default = []; 479 + }; 480 + 481 + persistence = mkOption { 482 + type = bool; 483 + description = '' 484 + Enable persistent storage of subscriptions and messages. 485 + ''; 486 + default = true; 487 + }; 488 489 + dataDir = mkOption { 490 + default = "/var/lib/mosquitto"; 491 + type = types.path; 492 + description = '' 493 + The data directory. 494 + ''; 495 + }; 496 497 + settings = mkOption { 498 + type = submodule { 499 + freeformType = attrsOf optionType; 500 + }; 501 + description = '' 502 + Global configuration options for the mosquitto broker. 503 + ''; 504 + default = {}; 505 }; 506 }; 507 508 + globalAsserts = prefix: cfg: 509 + flatten [ 510 + (assertKeysValid prefix freeformGlobalKeys cfg.settings) 511 + (imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners) 512 + (mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges) 513 + ]; 514 + 515 + formatGlobal = cfg: 516 + [ 517 + "per_listener_settings true" 518 + "persistence ${optionToString cfg.persistence}" 519 + ] 520 + ++ map 521 + (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}") 522 + cfg.logDest 523 + ++ map (t: "log_type ${t}") cfg.logType 524 + ++ formatFreeform {} cfg.settings 525 + ++ concatLists (imap0 formatListener cfg.listeners) 526 + ++ concatLists (mapAttrsToList formatBridge cfg.bridges) 527 + ++ map (d: "include_dir ${d}") cfg.includeDirs; 528 + 529 + configFile = pkgs.writeText "mosquitto.conf" 530 + (concatStringsSep "\n" (formatGlobal cfg)); 531 + 532 + in 533 + 534 + { 535 + 536 + ###### Interface 537 + 538 + options.services.mosquitto = globalOptions; 539 540 ###### Implementation 541 542 config = mkIf cfg.enable { 543 544 + assertions = globalAsserts "services.mosquitto" cfg; 545 546 systemd.services.mosquitto = { 547 description = "Mosquitto MQTT Broker Daemon"; ··· 555 RuntimeDirectory = "mosquitto"; 556 WorkingDirectory = cfg.dataDir; 557 Restart = "on-failure"; 558 + ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${configFile}"; 559 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 560 561 # Hardening ··· 580 ReadWritePaths = [ 581 cfg.dataDir 582 "/tmp" # mosquitto_passwd creates files in /tmp before moving them 583 + ] ++ filter path.check cfg.logDest; 584 + ReadOnlyPaths = 585 + map (p: "${p}") 586 + (cfg.includeDirs 587 + ++ filter 588 + (v: v != null) 589 + (flatten [ 590 + (map 591 + (l: [ 592 + (l.settings.psk_file or null) 593 + (l.settings.http_dir or null) 594 + (l.settings.cafile or null) 595 + (l.settings.capath or null) 596 + (l.settings.certfile or null) 597 + (l.settings.crlfile or null) 598 + (l.settings.dhparamfile or null) 599 + (l.settings.keyfile or null) 600 + ]) 601 + cfg.listeners) 602 + (mapAttrsToList 603 + (_: b: [ 604 + (b.settings.bridge_cafile or null) 605 + (b.settings.bridge_capath or null) 606 + (b.settings.bridge_certfile or null) 607 + (b.settings.bridge_keyfile or null) 608 + ]) 609 + cfg.bridges) 610 + ])); 611 RemoveIPC = true; 612 RestrictAddressFamilies = [ 613 "AF_UNIX" # for sd_notify() call ··· 625 ]; 626 UMask = "0077"; 627 }; 628 + preStart = 629 + concatStringsSep 630 + "\n" 631 + (imap0 632 + (idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}") 633 + cfg.listeners); 634 }; 635 636 users.users.mosquitto = { ··· 644 users.groups.mosquitto.gid = config.ids.gids.mosquitto; 645 646 }; 647 + 648 + meta.maintainers = with lib.maintainers; [ pennae ]; 649 }
+7 -6
nixos/tests/home-assistant.nix
··· 12 environment.systemPackages = with pkgs; [ mosquitto ]; 13 services.mosquitto = { 14 enable = true; 15 - checkPasswords = true; 16 - users = { 17 - "${mqttUsername}" = { 18 - acl = [ "topic readwrite #" ]; 19 - password = mqttPassword; 20 }; 21 - }; 22 }; 23 services.home-assistant = { 24 inherit configDir;
··· 12 environment.systemPackages = with pkgs; [ mosquitto ]; 13 services.mosquitto = { 14 enable = true; 15 + listeners = [ { 16 + users = { 17 + "${mqttUsername}" = { 18 + acl = [ "readwrite #" ]; 19 + password = mqttPassword; 20 + }; 21 }; 22 + } ]; 23 }; 24 services.home-assistant = { 25 inherit configDir;
+11 -9
nixos/tests/mosquitto.nix
··· 19 server = { pkgs, ... }: { 20 networking.firewall.allowedTCPPorts = [ port ]; 21 services.mosquitto = { 22 - inherit port; 23 enable = true; 24 - host = "0.0.0.0"; 25 - checkPasswords = true; 26 - users.${username} = { 27 - inherit password; 28 - acl = [ 29 - "topic readwrite ${topic}" 30 - ]; 31 - }; 32 }; 33 34 # disable private /tmp for this test
··· 19 server = { pkgs, ... }: { 20 networking.firewall.allowedTCPPorts = [ port ]; 21 services.mosquitto = { 22 enable = true; 23 + listeners = [ 24 + { 25 + inherit port; 26 + users.${username} = { 27 + inherit password; 28 + acl = [ 29 + "readwrite ${topic}" 30 + ]; 31 + }; 32 + } 33 + ]; 34 }; 35 36 # disable private /tmp for this test