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 1138 <literal>coursier</literal>, you can create a shell alias. 1139 1139 </para> 1140 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> 1141 1149 </itemizedlist> 1142 1150 </section> 1143 1151 <section xml:id="sec-release-21.11-notable-changes">
+3
nixos/doc/manual/release-notes/rl-2111.section.md
··· 351 351 352 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 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 + 354 357 ## Other Notable Changes {#sec-release-21.11-notable-changes} 355 358 356 359
+511 -167
nixos/modules/services/networking/mosquitto.nix
··· 5 5 let 6 6 cfg = config.services.mosquitto; 7 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 - ''; 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; 14 23 15 - passwordConf = optionalString cfg.checkPasswords '' 16 - password_file ${cfg.dataDir}/passwd 17 - ''; 24 + assertKeysValid = prefix: valid: config: 25 + mapAttrsToList 26 + (n: _: { 27 + assertion = valid ? ${n}; 28 + message = "Invalid config key ${prefix}.${n}."; 29 + }) 30 + config; 18 31 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 - ''; 32 + formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}"); 28 33 29 - userAcl = (concatStringsSep "\n\n" (mapAttrsToList (n: c: 30 - "user ${n}\n" + (concatStringsSep "\n" c.acl)) cfg.users 31 - )); 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 + }; 32 43 33 - aclFile = pkgs.writeText "mosquitto.acl" '' 34 - ${cfg.aclExtraConf} 35 - ${userAcl} 36 - ''; 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 + }; 37 53 38 - in 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 + }; 39 63 40 - { 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 + }; 41 75 42 - ###### Interface 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} 43 121 44 - options = { 45 - services.mosquitto = { 46 - enable = mkEnableOption "the MQTT Mosquitto broker"; 122 + rm -f "$file" 123 + touch "$file" 47 124 48 - host = mkOption { 49 - default = "127.0.0.1"; 50 - example = "0.0.0.0"; 51 - type = types.str; 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; 52 158 description = '' 53 - Host to listen on without SSL. 159 + Plugin path to load, should be a <literal>.so</literal> file. 54 160 ''; 55 161 }; 56 162 57 - port = mkOption { 58 - default = 1883; 59 - type = types.int; 163 + denySpecialChars = mkOption { 164 + type = bool; 60 165 description = '' 61 - Port on which to listen without SSL. 166 + Automatically disallow all clients using <literal>#</literal> 167 + or <literal>+</literal> in their name/id. 62 168 ''; 169 + default = true; 63 170 }; 64 171 65 - ssl = { 66 - enable = mkEnableOption "SSL listener"; 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 + }; 67 182 68 - cafile = mkOption { 69 - type = types.nullOr types.path; 70 - default = null; 71 - description = "Path to PEM encoded CA certificates."; 72 - }; 183 + authAsserts = prefix: auth: 184 + mapAttrsToList 185 + (n: _: { 186 + assertion = configKey.check n; 187 + message = "Invalid auth plugin key ${prefix}.${n}"; 188 + }) 189 + auth; 73 190 74 - certfile = mkOption { 75 - type = types.nullOr types.path; 76 - default = null; 77 - description = "Path to PEM encoded server certificate."; 78 - }; 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; 79 197 80 - keyfile = mkOption { 81 - type = types.nullOr types.path; 82 - default = null; 83 - description = "Path to PEM encoded server key."; 84 - }; 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 + }; 85 228 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 - }; 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 + }; 94 238 95 - port = mkOption { 96 - default = 8883; 97 - type = types.int; 98 - description = '' 99 - Port on which to listen with SSL. 100 - ''; 101 - }; 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; 102 246 }; 103 247 104 - dataDir = mkOption { 105 - default = "/var/lib/mosquitto"; 106 - type = types.path; 248 + authPlugins = mkOption { 249 + type = listOf authPluginOptions; 107 250 description = '' 108 - The data directory. 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. 109 254 ''; 255 + default = []; 110 256 }; 111 257 112 258 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 - }; 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 + }; 122 266 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 - }; 267 + acl = mkOption { 268 + type = listOf str; 269 + description = '' 270 + Additional ACL items to prepend to the generated ACL file. 271 + ''; 272 + default = []; 273 + }; 132 274 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 - }; 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 + }; 142 286 143 - hashedPasswordFile = mkOption { 144 - type = with types; uniq (nullOr str); 145 - example = "/path/to/file"; 146 - default = null; 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; 147 346 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>. 347 + Address of the remote MQTT broker. 152 348 ''; 153 349 }; 154 350 155 - acl = mkOption { 156 - type = types.listOf types.str; 157 - example = [ "topic read A/B" "topic A/#" ]; 351 + port = mkOption { 352 + type = port; 158 353 description = '' 159 - Control client access to topics on the broker. 354 + Port of the remote MQTT broker. 160 355 ''; 356 + default = 1883; 161 357 }; 162 358 }; 163 359 }); 164 - example = { john = { password = "123456"; acl = [ "topic readwrite john/#" ]; }; }; 360 + default = []; 165 361 description = '' 166 - A set of users and their passwords and ACLs. 362 + Remote endpoints for the bridge. 167 363 ''; 168 364 }; 169 365 170 - allowAnonymous = mkOption { 171 - default = false; 172 - type = types.bool; 366 + topics = mkOption { 367 + type = listOf str; 173 368 description = '' 174 - Allow clients to connect without authentication. 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. 175 372 ''; 373 + default = []; 374 + example = [ "# both 2 local/topic/ remote/topic/" ]; 176 375 }; 177 376 178 - checkPasswords = mkOption { 179 - default = false; 180 - example = true; 181 - type = types.bool; 377 + settings = mkOption { 378 + type = submodule { 379 + freeformType = attrsOf optionType; 380 + }; 182 381 description = '' 183 - Refuse connection when clients provide incorrect passwords. 382 + Additional settings for this bridge. 184 383 ''; 384 + default = {}; 185 385 }; 386 + }; 387 + }; 186 388 187 - extraConf = mkOption { 188 - default = ""; 189 - type = types.lines; 190 - description = '' 191 - Extra config to append to `mosquitto.conf` file. 192 - ''; 193 - }; 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 + }; 194 488 195 - aclExtraConf = mkOption { 196 - default = ""; 197 - type = types.lines; 198 - description = '' 199 - Extra config to prepend to the ACL file. 200 - ''; 201 - }; 489 + dataDir = mkOption { 490 + default = "/var/lib/mosquitto"; 491 + type = types.path; 492 + description = '' 493 + The data directory. 494 + ''; 495 + }; 202 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 = {}; 203 505 }; 204 506 }; 205 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; 206 539 207 540 ###### Implementation 208 541 209 542 config = mkIf cfg.enable { 210 543 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; 544 + assertions = globalAsserts "services.mosquitto" cfg; 217 545 218 546 systemd.services.mosquitto = { 219 547 description = "Mosquitto MQTT Broker Daemon"; ··· 227 555 RuntimeDirectory = "mosquitto"; 228 556 WorkingDirectory = cfg.dataDir; 229 557 Restart = "on-failure"; 230 - ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}"; 558 + ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${configFile}"; 231 559 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 232 560 233 561 # Hardening ··· 252 580 ReadWritePaths = [ 253 581 cfg.dataDir 254 582 "/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 - ]; 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 + ])); 261 611 RemoveIPC = true; 262 612 RestrictAddressFamilies = [ 263 613 "AF_UNIX" # for sd_notify() call ··· 275 625 ]; 276 626 UMask = "0077"; 277 627 }; 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); 628 + preStart = 629 + concatStringsSep 630 + "\n" 631 + (imap0 632 + (idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}") 633 + cfg.listeners); 292 634 }; 293 635 294 636 users.users.mosquitto = { ··· 302 644 users.groups.mosquitto.gid = config.ids.gids.mosquitto; 303 645 304 646 }; 647 + 648 + meta.maintainers = with lib.maintainers; [ pennae ]; 305 649 }
+7 -6
nixos/tests/home-assistant.nix
··· 12 12 environment.systemPackages = with pkgs; [ mosquitto ]; 13 13 services.mosquitto = { 14 14 enable = true; 15 - checkPasswords = true; 16 - users = { 17 - "${mqttUsername}" = { 18 - acl = [ "topic readwrite #" ]; 19 - password = mqttPassword; 15 + listeners = [ { 16 + users = { 17 + "${mqttUsername}" = { 18 + acl = [ "readwrite #" ]; 19 + password = mqttPassword; 20 + }; 20 21 }; 21 - }; 22 + } ]; 22 23 }; 23 24 services.home-assistant = { 24 25 inherit configDir;
+11 -9
nixos/tests/mosquitto.nix
··· 19 19 server = { pkgs, ... }: { 20 20 networking.firewall.allowedTCPPorts = [ port ]; 21 21 services.mosquitto = { 22 - inherit port; 23 22 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 - }; 23 + listeners = [ 24 + { 25 + inherit port; 26 + users.${username} = { 27 + inherit password; 28 + acl = [ 29 + "readwrite ${topic}" 30 + ]; 31 + }; 32 + } 33 + ]; 32 34 }; 33 35 34 36 # disable private /tmp for this test