1{ config, lib, pkgs, ...}:
2
3with lib;
4
5let
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
38in
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 example = 1883;
60 type = types.int;
61 description = ''
62 Port on which to listen without SSL.
63 '';
64 };
65
66 ssl = {
67 enable = mkEnableOption "SSL listener";
68
69 cafile = mkOption {
70 type = types.nullOr types.path;
71 default = null;
72 description = "Path to PEM encoded CA certificates.";
73 };
74
75 certfile = mkOption {
76 type = types.nullOr types.path;
77 default = null;
78 description = "Path to PEM encoded server certificate.";
79 };
80
81 keyfile = mkOption {
82 type = types.nullOr types.path;
83 default = null;
84 description = "Path to PEM encoded server key.";
85 };
86
87 host = mkOption {
88 default = "0.0.0.0";
89 example = "localhost";
90 type = types.str;
91 description = ''
92 Host to listen on with SSL.
93 '';
94 };
95
96 port = mkOption {
97 default = 8883;
98 example = 8883;
99 type = types.int;
100 description = ''
101 Port on which to listen with SSL.
102 '';
103 };
104 };
105
106 dataDir = mkOption {
107 default = "/var/lib/mosquitto";
108 type = types.path;
109 description = ''
110 The data directory.
111 '';
112 };
113
114 users = mkOption {
115 type = types.attrsOf (types.submodule {
116 options = {
117 password = mkOption {
118 type = with types; uniq (nullOr str);
119 default = null;
120 description = ''
121 Specifies the (clear text) password for the MQTT User.
122 '';
123 };
124
125 passwordFile = mkOption {
126 type = with types; uniq (nullOr str);
127 example = "/path/to/file";
128 default = null;
129 description = ''
130 Specifies the path to a file containing the
131 clear text password for the MQTT user.
132 '';
133 };
134
135 hashedPassword = mkOption {
136 type = with types; uniq (nullOr str);
137 default = null;
138 description = ''
139 Specifies the hashed password for the MQTT User.
140 To generate hashed password install <literal>mosquitto</literal>
141 package and use <literal>mosquitto_passwd</literal>.
142 '';
143 };
144
145 hashedPasswordFile = mkOption {
146 type = with types; uniq (nullOr str);
147 example = "/path/to/file";
148 default = null;
149 description = ''
150 Specifies the path to a file containing the
151 hashed password for the MQTT user.
152 To generate hashed password install <literal>mosquitto</literal>
153 package and use <literal>mosquitto_passwd</literal>.
154 '';
155 };
156
157 acl = mkOption {
158 type = types.listOf types.str;
159 example = [ "topic read A/B" "topic A/#" ];
160 description = ''
161 Control client access to topics on the broker.
162 '';
163 };
164 };
165 });
166 example = { john = { password = "123456"; acl = [ "topic readwrite john/#" ]; }; };
167 description = ''
168 A set of users and their passwords and ACLs.
169 '';
170 };
171
172 allowAnonymous = mkOption {
173 default = false;
174 type = types.bool;
175 description = ''
176 Allow clients to connect without authentication.
177 '';
178 };
179
180 checkPasswords = mkOption {
181 default = false;
182 example = true;
183 type = types.bool;
184 description = ''
185 Refuse connection when clients provide incorrect passwords.
186 '';
187 };
188
189 extraConf = mkOption {
190 default = "";
191 type = types.lines;
192 description = ''
193 Extra config to append to `mosquitto.conf` file.
194 '';
195 };
196
197 aclExtraConf = mkOption {
198 default = "";
199 type = types.lines;
200 description = ''
201 Extra config to prepend to the ACL file.
202 '';
203 };
204
205 };
206 };
207
208
209 ###### Implementation
210
211 config = mkIf cfg.enable {
212
213 assertions = mapAttrsToList (name: cfg: {
214 assertion = length (filter (s: s != null) (with cfg; [
215 password passwordFile hashedPassword hashedPasswordFile
216 ])) <= 1;
217 message = "Cannot set more than one password option";
218 }) cfg.users;
219
220 systemd.services.mosquitto = {
221 description = "Mosquitto MQTT Broker Daemon";
222 wantedBy = [ "multi-user.target" ];
223 after = [ "network.target" ];
224 serviceConfig = {
225 Type = "notify";
226 NotifyAccess = "main";
227 User = "mosquitto";
228 Group = "mosquitto";
229 RuntimeDirectory = "mosquitto";
230 WorkingDirectory = cfg.dataDir;
231 Restart = "on-failure";
232 ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
233 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
234
235 # Hardening
236 CapabilityBoundingSet = "";
237 DevicePolicy = "closed";
238 LockPersonality = true;
239 MemoryDenyWriteExecute = true;
240 NoNewPrivileges = true;
241 PrivateDevices = true;
242 PrivateTmp = true;
243 PrivateUsers = true;
244 ProtectClock = true;
245 ProtectControlGroups = true;
246 ProtectHome = true;
247 ProtectHostname = true;
248 ProtectKernelLogs = true;
249 ProtectKernelModules = true;
250 ProtectKernelTunables = true;
251 ProtectProc = "invisible";
252 ProcSubset = "pid";
253 ProtectSystem = "strict";
254 ReadWritePaths = [
255 cfg.dataDir
256 "/tmp" # mosquitto_passwd creates files in /tmp before moving them
257 ];
258 ReadOnlyPaths = with cfg.ssl; lib.optionals (enable) [
259 certfile
260 keyfile
261 cafile
262 ];
263 RemoveIPC = true;
264 RestrictAddressFamilies = [
265 "AF_UNIX" # for sd_notify() call
266 "AF_INET"
267 "AF_INET6"
268 ];
269 RestrictNamespaces = true;
270 RestrictRealtime = true;
271 RestrictSUIDSGID = true;
272 SystemCallArchitectures = "native";
273 SystemCallFilter = [
274 "@system-service"
275 "~@privileged"
276 "~@resources"
277 ];
278 UMask = "0077";
279 };
280 preStart = ''
281 rm -f ${cfg.dataDir}/passwd
282 touch ${cfg.dataDir}/passwd
283 '' + concatStringsSep "\n" (
284 mapAttrsToList (n: c:
285 if c.hashedPasswordFile != null then
286 "echo '${n}:'$(cat '${c.hashedPasswordFile}') >> ${cfg.dataDir}/passwd"
287 else if c.passwordFile != null then
288 "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} $(cat '${c.passwordFile}')"
289 else if c.hashedPassword != null then
290 "echo '${n}:${c.hashedPassword}' >> ${cfg.dataDir}/passwd"
291 else optionalString (c.password != null)
292 "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} '${c.password}'"
293 ) cfg.users);
294 };
295
296 users.users.mosquitto = {
297 description = "Mosquitto MQTT Broker Daemon owner";
298 group = "mosquitto";
299 uid = config.ids.uids.mosquitto;
300 home = cfg.dataDir;
301 createHome = true;
302 };
303
304 users.groups.mosquitto.gid = config.ids.gids.mosquitto;
305
306 };
307}