1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.redis;
9
10 mkValueString =
11 value:
12 if value == true then
13 "yes"
14 else if value == false then
15 "no"
16 else
17 lib.generators.mkValueStringDefault { } value;
18
19 redisConfig =
20 settings:
21 pkgs.writeText "redis.conf" (
22 lib.generators.toKeyValue {
23 listsAsDuplicateKeys = true;
24 mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " ";
25 } settings
26 );
27
28 redisName = name: "redis" + lib.optionalString (name != "") ("-" + name);
29 enabledServers = lib.filterAttrs (name: conf: conf.enable) config.services.redis.servers;
30
31in
32{
33 imports = [
34 (lib.mkRemovedOptionModule [
35 "services"
36 "redis"
37 "user"
38 ] "The redis module now is hardcoded to the redis user.")
39 (lib.mkRemovedOptionModule [
40 "services"
41 "redis"
42 "dbpath"
43 ] "The redis module now uses /var/lib/redis as data directory.")
44 (lib.mkRemovedOptionModule [
45 "services"
46 "redis"
47 "dbFilename"
48 ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
49 (lib.mkRemovedOptionModule [
50 "services"
51 "redis"
52 "appendOnlyFilename"
53 ] "This option was never used.")
54 (lib.mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
55 (lib.mkRemovedOptionModule [
56 "services"
57 "redis"
58 "extraConfig"
59 ] "Use services.redis.servers.*.settings instead.")
60 (lib.mkRenamedOptionModule
61 [ "services" "redis" "enable" ]
62 [ "services" "redis" "servers" "" "enable" ]
63 )
64 (lib.mkRenamedOptionModule [ "services" "redis" "port" ] [ "services" "redis" "servers" "" "port" ])
65 (lib.mkRenamedOptionModule
66 [ "services" "redis" "openFirewall" ]
67 [ "services" "redis" "servers" "" "openFirewall" ]
68 )
69 (lib.mkRenamedOptionModule [ "services" "redis" "bind" ] [ "services" "redis" "servers" "" "bind" ])
70 (lib.mkRenamedOptionModule
71 [ "services" "redis" "unixSocket" ]
72 [ "services" "redis" "servers" "" "unixSocket" ]
73 )
74 (lib.mkRenamedOptionModule
75 [ "services" "redis" "unixSocketPerm" ]
76 [ "services" "redis" "servers" "" "unixSocketPerm" ]
77 )
78 (lib.mkRenamedOptionModule
79 [ "services" "redis" "logLevel" ]
80 [ "services" "redis" "servers" "" "logLevel" ]
81 )
82 (lib.mkRenamedOptionModule
83 [ "services" "redis" "logfile" ]
84 [ "services" "redis" "servers" "" "logfile" ]
85 )
86 (lib.mkRenamedOptionModule
87 [ "services" "redis" "syslog" ]
88 [ "services" "redis" "servers" "" "syslog" ]
89 )
90 (lib.mkRenamedOptionModule
91 [ "services" "redis" "databases" ]
92 [ "services" "redis" "servers" "" "databases" ]
93 )
94 (lib.mkRenamedOptionModule
95 [ "services" "redis" "maxclients" ]
96 [ "services" "redis" "servers" "" "maxclients" ]
97 )
98 (lib.mkRenamedOptionModule [ "services" "redis" "save" ] [ "services" "redis" "servers" "" "save" ])
99 (lib.mkRenamedOptionModule
100 [ "services" "redis" "slaveOf" ]
101 [ "services" "redis" "servers" "" "slaveOf" ]
102 )
103 (lib.mkRenamedOptionModule
104 [ "services" "redis" "masterAuth" ]
105 [ "services" "redis" "servers" "" "masterAuth" ]
106 )
107 (lib.mkRenamedOptionModule
108 [ "services" "redis" "requirePass" ]
109 [ "services" "redis" "servers" "" "requirePass" ]
110 )
111 (lib.mkRenamedOptionModule
112 [ "services" "redis" "requirePassFile" ]
113 [ "services" "redis" "servers" "" "requirePassFile" ]
114 )
115 (lib.mkRenamedOptionModule
116 [ "services" "redis" "appendOnly" ]
117 [ "services" "redis" "servers" "" "appendOnly" ]
118 )
119 (lib.mkRenamedOptionModule
120 [ "services" "redis" "appendFsync" ]
121 [ "services" "redis" "servers" "" "appendFsync" ]
122 )
123 (lib.mkRenamedOptionModule
124 [ "services" "redis" "slowLogLogSlowerThan" ]
125 [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ]
126 )
127 (lib.mkRenamedOptionModule
128 [ "services" "redis" "slowLogMaxLen" ]
129 [ "services" "redis" "servers" "" "slowLogMaxLen" ]
130 )
131 (lib.mkRenamedOptionModule
132 [ "services" "redis" "settings" ]
133 [ "services" "redis" "servers" "" "settings" ]
134 )
135 ];
136
137 ###### interface
138
139 options = {
140
141 services.redis = {
142 package = lib.mkPackageOption pkgs "redis" { };
143
144 vmOverCommit =
145 lib.mkEnableOption ''
146 set `vm.overcommit_memory` sysctl to 1
147 (Suggested for Background Saving: <https://redis.io/docs/get-started/faq/>)
148 ''
149 // {
150 default = true;
151 };
152
153 servers = lib.mkOption {
154 type =
155 with lib.types;
156 attrsOf (
157 submodule (
158 { config, name, ... }:
159 {
160 options = {
161 enable = lib.mkEnableOption "Redis server";
162
163 user = lib.mkOption {
164 type = types.str;
165 default = redisName name;
166 defaultText = lib.literalExpression ''
167 if name == "" then "redis" else "redis-''${name}"
168 '';
169 description = ''
170 User account under which this instance of redis-server runs.
171
172 ::: {.note}
173 If left as the default value this user will automatically be
174 created on system activation, otherwise you are responsible for
175 ensuring the user exists before the redis service starts.
176 '';
177 };
178
179 group = lib.mkOption {
180 type = types.str;
181 default = config.user;
182 defaultText = lib.literalExpression "config.user";
183 description = ''
184 Group account under which this instance of redis-server runs.
185
186 ::: {.note}
187 If left as the default value this group will automatically be
188 created on system activation, otherwise you are responsible for
189 ensuring the group exists before the redis service starts.
190 '';
191 };
192
193 port = lib.mkOption {
194 type = types.port;
195 default = if name == "" then 6379 else 0;
196 defaultText = lib.literalExpression ''if name == "" then 6379 else 0'';
197 description = ''
198 The TCP port to accept connections.
199 If port 0 is specified Redis will not listen on a TCP socket.
200 '';
201 };
202
203 openFirewall = lib.mkOption {
204 type = types.bool;
205 default = false;
206 description = ''
207 Whether to open ports in the firewall for the server.
208 '';
209 };
210
211 extraParams = lib.mkOption {
212 type = with types; listOf str;
213 default = [ ];
214 description = "Extra parameters to append to redis-server invocation";
215 example = [ "--sentinel" ];
216 };
217
218 bind = lib.mkOption {
219 type = with types; nullOr str;
220 default = "127.0.0.1";
221 description = ''
222 The IP interface to bind to.
223 `null` means "all interfaces".
224 '';
225 example = "192.0.2.1";
226 };
227
228 unixSocket = lib.mkOption {
229 type = with types; nullOr path;
230 default = "/run/${redisName name}/redis.sock";
231 defaultText = lib.literalExpression ''
232 if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock"
233 '';
234 description = "The path to the socket to bind to.";
235 };
236
237 unixSocketPerm = lib.mkOption {
238 type = types.int;
239 default = 660;
240 description = "Change permissions for the socket";
241 example = 600;
242 };
243
244 logLevel = lib.mkOption {
245 type = types.str;
246 default = "notice"; # debug, verbose, notice, warning
247 example = "debug";
248 description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
249 };
250
251 logfile = lib.mkOption {
252 type = types.str;
253 default = "/dev/null";
254 description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
255 example = "/var/log/redis.log";
256 };
257
258 syslog = lib.mkOption {
259 type = types.bool;
260 default = true;
261 description = "Enable logging to the system logger.";
262 };
263
264 databases = lib.mkOption {
265 type = types.int;
266 default = 16;
267 description = "Set the number of databases.";
268 };
269
270 maxclients = lib.mkOption {
271 type = types.int;
272 default = 10000;
273 description = "Set the max number of connected clients at the same time.";
274 };
275
276 save = lib.mkOption {
277 type = with types; listOf (listOf int);
278 default = [
279 [
280 900
281 1
282 ]
283 [
284 300
285 10
286 ]
287 [
288 60
289 10000
290 ]
291 ];
292 description = ''
293 The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.
294
295 If set to the empty list (`[]`) then RDB persistence will be disabled (useful if you are using AOF or don't want any persistence).
296 '';
297 };
298
299 slaveOf = lib.mkOption {
300 type =
301 with types;
302 nullOr (
303 submodule (
304 { ... }:
305 {
306 options = {
307 ip = lib.mkOption {
308 type = str;
309 description = "IP of the Redis master";
310 example = "192.168.1.100";
311 };
312
313 port = lib.mkOption {
314 type = port;
315 description = "port of the Redis master";
316 default = 6379;
317 };
318 };
319 }
320 )
321 );
322
323 default = null;
324 description = "IP and port to which this redis instance acts as a slave.";
325 example = {
326 ip = "192.168.1.100";
327 port = 6379;
328 };
329 };
330
331 masterAuth = lib.mkOption {
332 type = with types; nullOr str;
333 default = null;
334 description = ''
335 If the master is password protected (using the requirePass configuration)
336 it is possible to tell the slave to authenticate before starting the replication synchronization
337 process, otherwise the master will refuse the slave request.
338 (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
339 };
340
341 requirePass = lib.mkOption {
342 type = with types; nullOr str;
343 default = null;
344 description = ''
345 Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
346 Use requirePassFile to store it outside of the nix store in a dedicated file.
347 '';
348 example = "letmein!";
349 };
350
351 requirePassFile = lib.mkOption {
352 type = with types; nullOr path;
353 default = null;
354 description = "File with password for the database.";
355 example = "/run/keys/redis-password";
356 };
357
358 appendOnly = lib.mkOption {
359 type = types.bool;
360 default = false;
361 description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
362 };
363
364 appendFsync = lib.mkOption {
365 type = types.str;
366 default = "everysec"; # no, always, everysec
367 description = "How often to fsync the append-only log, options: no, always, everysec.";
368 };
369
370 slowLogLogSlowerThan = lib.mkOption {
371 type = types.int;
372 default = 10000;
373 description = "Log queries whose execution take longer than X in milliseconds.";
374 example = 1000;
375 };
376
377 slowLogMaxLen = lib.mkOption {
378 type = types.int;
379 default = 128;
380 description = "Maximum number of items to keep in slow log.";
381 };
382
383 settings = lib.mkOption {
384 # TODO: this should be converted to freeformType
385 type =
386 with types;
387 attrsOf (oneOf [
388 bool
389 int
390 str
391 (listOf str)
392 ]);
393 default = { };
394 description = ''
395 Redis configuration. Refer to
396 <https://redis.io/topics/config>
397 for details on supported values.
398 '';
399 example = lib.literalExpression ''
400 {
401 loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
402 }
403 '';
404 };
405 };
406 config.settings = lib.mkMerge [
407 {
408 inherit (config)
409 port
410 logfile
411 databases
412 maxclients
413 appendOnly
414 ;
415 daemonize = false;
416 supervised = "systemd";
417 loglevel = config.logLevel;
418 syslog-enabled = config.syslog;
419 save =
420 if config.save == [ ] then
421 ''""'' # Disable saving with `save = ""`
422 else
423 map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
424 dbfilename = "dump.rdb";
425 dir = "/var/lib/${redisName name}";
426 appendfsync = config.appendFsync;
427 slowlog-log-slower-than = config.slowLogLogSlowerThan;
428 slowlog-max-len = config.slowLogMaxLen;
429 }
430 (lib.mkIf (config.bind != null) { inherit (config) bind; })
431 (lib.mkIf (config.unixSocket != null) {
432 unixsocket = config.unixSocket;
433 unixsocketperm = toString config.unixSocketPerm;
434 })
435 (lib.mkIf (config.slaveOf != null) {
436 slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}";
437 })
438 (lib.mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
439 (lib.mkIf (config.requirePass != null) { requirepass = config.requirePass; })
440 ];
441 }
442 )
443 );
444 description = "Configuration of multiple `redis-server` instances.";
445 default = { };
446 };
447 };
448
449 };
450
451 ###### implementation
452
453 config = lib.mkIf (enabledServers != { }) {
454
455 assertions = lib.attrValues (
456 lib.mapAttrs (name: conf: {
457 assertion = conf.requirePass != null -> conf.requirePassFile == null;
458 message = ''
459 You can only set one services.redis.servers.${name}.requirePass
460 or services.redis.servers.${name}.requirePassFile
461 '';
462 }) enabledServers
463 );
464
465 boot.kernel.sysctl = lib.mkIf cfg.vmOverCommit {
466 "vm.overcommit_memory" = "1";
467 };
468
469 networking.firewall.allowedTCPPorts = lib.concatMap (
470 conf: lib.optional conf.openFirewall conf.port
471 ) (lib.attrValues enabledServers);
472
473 environment.systemPackages = [ cfg.package ];
474
475 users.users = lib.mapAttrs' (
476 name: conf:
477 lib.nameValuePair (redisName name) {
478 description = "System user for the redis-server instance ${name}";
479 isSystemUser = true;
480 group = redisName name;
481 }
482 ) enabledServers;
483 users.groups = lib.mapAttrs' (
484 name: conf:
485 lib.nameValuePair (redisName name) {
486 }
487 ) enabledServers;
488
489 systemd.services = lib.mapAttrs' (
490 name: conf:
491 lib.nameValuePair (redisName name) {
492 description = "Redis Server - ${redisName name}";
493
494 wantedBy = [ "multi-user.target" ];
495 after = [ "network.target" ];
496
497 serviceConfig = {
498 ExecStart = "${cfg.package}/bin/${
499 cfg.package.serverBin or "redis-server"
500 } /var/lib/${redisName name}/redis.conf ${lib.escapeShellArgs conf.extraParams}";
501 ExecStartPre =
502 "+"
503 + pkgs.writeShellScript "${redisName name}-prep-conf" (
504 let
505 redisConfVar = "/var/lib/${redisName name}/redis.conf";
506 redisConfRun = "/run/${redisName name}/nixos.conf";
507 redisConfStore = redisConfig conf.settings;
508 in
509 ''
510 touch "${redisConfVar}" "${redisConfRun}"
511 chown '${conf.user}':'${conf.group}' "${redisConfVar}" "${redisConfRun}"
512 chmod 0600 "${redisConfVar}" "${redisConfRun}"
513 if [ ! -s ${redisConfVar} ]; then
514 echo 'include "${redisConfRun}"' > "${redisConfVar}"
515 fi
516 echo 'include "${redisConfStore}"' > "${redisConfRun}"
517 ${lib.optionalString (conf.requirePassFile != null) ''
518 {
519 echo -n "requirepass "
520 cat ${lib.escapeShellArg conf.requirePassFile}
521 } >> "${redisConfRun}"
522 ''}
523 ''
524 );
525 Type = "notify";
526 # User and group
527 User = conf.user;
528 Group = conf.group;
529 # Runtime directory and mode
530 RuntimeDirectory = redisName name;
531 RuntimeDirectoryMode = "0750";
532 # State directory and mode
533 StateDirectory = redisName name;
534 StateDirectoryMode = "0700";
535 # Access write directories
536 UMask = "0077";
537 # Capabilities
538 CapabilityBoundingSet = "";
539 # Security
540 NoNewPrivileges = true;
541 # Process Properties
542 LimitNOFILE = lib.mkDefault "${toString (conf.maxclients + 32)}";
543 # Sandboxing
544 ProtectSystem = "strict";
545 ProtectHome = true;
546 PrivateTmp = true;
547 PrivateDevices = true;
548 PrivateUsers = true;
549 ProtectClock = true;
550 ProtectHostname = true;
551 ProtectKernelLogs = true;
552 ProtectKernelModules = true;
553 ProtectKernelTunables = true;
554 ProtectControlGroups = true;
555 RestrictAddressFamilies = [
556 "AF_INET"
557 "AF_INET6"
558 "AF_UNIX"
559 ];
560 RestrictNamespaces = true;
561 LockPersonality = true;
562 # we need to disable MemoryDenyWriteExecute for keydb
563 MemoryDenyWriteExecute = cfg.package.pname != "keydb";
564 RestrictRealtime = true;
565 RestrictSUIDSGID = true;
566 PrivateMounts = true;
567 # System Call Filtering
568 SystemCallArchitectures = "native";
569 SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
570 };
571 }
572 ) enabledServers;
573
574 };
575}