at 25.11-pre 21 kB view raw
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}