nixos/redis: enable multiple instances of redis-server

main
Julien Moutinho 3 years ago committed by tomberek
parent f40283cf62
commit 7475554372
  1. 29
      nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
  2. 16
      nixos/doc/manual/release-notes/rl-2205.section.md
  3. 488
      nixos/modules/services/databases/redis.nix

@ -87,7 +87,32 @@
</section>
<section xml:id="sec-release-22.05-notable-changes">
<title>Other Notable Changes</title>
<para>
</para>
<itemizedlist spacing="compact">
<listitem>
<para>
The option
<link linkend="opt-services.redis.servers">services.redis.servers</link>
was added to support per-application
<literal>redis-server</literal> which is more secure since
Redis databases are only mere key prefixes without any
configuration or ACL of their own. Backward-compatibility is
preserved by mapping old
<literal>services.redis.settings</literal> to
<literal>services.redis.servers.&quot;&quot;.settings</literal>,
but you are strongly encouraged to name each
<literal>redis-server</literal> instance after the application
using it, instead of keeping that nameless one. Except for the
nameless
<literal>services.redis.servers.&quot;&quot;</literal> still
accessible at <literal>127.0.0.1:6379</literal>, and to the
members of the Unix group <literal>redis</literal> through the
Unix socket <literal>/run/redis/redis.sock</literal>, all
other <literal>services.redis.servers.${serverName}</literal>
are only accessible by default to the members of the Unix
group <literal>redis-${serverName}</literal> through the Unix
socket <literal>/run/redis-${serverName}/redis.sock</literal>.
</para>
</listitem>
</itemizedlist>
</section>
</section>

@ -35,3 +35,19 @@ In addition to numerous new and upgraded packages, this release has the followin
Please switch to `claws-mail`, which is Claws Mail's latest release based on GTK+3 and Python 3.
## Other Notable Changes {#sec-release-22.05-notable-changes}
- The option [services.redis.servers](#opt-services.redis.servers) was added
to support per-application `redis-server` which is more secure since Redis databases
are only mere key prefixes without any configuration or ACL of their own.
Backward-compatibility is preserved by mapping old `services.redis.settings`
to `services.redis.servers."".settings`, but you are strongly encouraged
to name each `redis-server` instance after the application using it,
instead of keeping that nameless one.
Except for the nameless `services.redis.servers.""`
still accessible at `127.0.0.1:6379`,
and to the members of the Unix group `redis`
through the Unix socket `/run/redis/redis.sock`,
all other `services.redis.servers.${serverName}`
are only accessible by default
to the members of the Unix group `redis-${serverName}`
through the Unix socket `/run/redis-${serverName}/redis.sock`.

@ -5,17 +5,18 @@ with lib;
let
cfg = config.services.redis;
ulimitNofile = cfg.maxclients + 32;
mkValueString = value:
if value == true then "yes"
else if value == false then "no"
else generators.mkValueStringDefault { } value;
redisConfig = pkgs.writeText "redis.conf" (generators.toKeyValue {
redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue {
listsAsDuplicateKeys = true;
mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
} cfg.settings);
} settings);
redisName = name: "redis" + optionalString (name != "") ("-"+name);
enabledServers = filterAttrs (name: conf: conf.enable) config.services.redis.servers;
in {
imports = [
@ -24,7 +25,28 @@ in {
(mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
(mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
(mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
(mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
(mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.servers.*.settings instead.")
(mkRenamedOptionModule [ "services" "redis" "enable"] [ "services" "redis" "servers" "" "enable" ])
(mkRenamedOptionModule [ "services" "redis" "port"] [ "services" "redis" "servers" "" "port" ])
(mkRenamedOptionModule [ "services" "redis" "openFirewall"] [ "services" "redis" "servers" "" "openFirewall" ])
(mkRenamedOptionModule [ "services" "redis" "bind"] [ "services" "redis" "servers" "" "bind" ])
(mkRenamedOptionModule [ "services" "redis" "unixSocket"] [ "services" "redis" "servers" "" "unixSocket" ])
(mkRenamedOptionModule [ "services" "redis" "unixSocketPerm"] [ "services" "redis" "servers" "" "unixSocketPerm" ])
(mkRenamedOptionModule [ "services" "redis" "logLevel"] [ "services" "redis" "servers" "" "logLevel" ])
(mkRenamedOptionModule [ "services" "redis" "logfile"] [ "services" "redis" "servers" "" "logfile" ])
(mkRenamedOptionModule [ "services" "redis" "syslog"] [ "services" "redis" "servers" "" "syslog" ])
(mkRenamedOptionModule [ "services" "redis" "databases"] [ "services" "redis" "servers" "" "databases" ])
(mkRenamedOptionModule [ "services" "redis" "maxclients"] [ "services" "redis" "servers" "" "maxclients" ])
(mkRenamedOptionModule [ "services" "redis" "save"] [ "services" "redis" "servers" "" "save" ])
(mkRenamedOptionModule [ "services" "redis" "slaveOf"] [ "services" "redis" "servers" "" "slaveOf" ])
(mkRenamedOptionModule [ "services" "redis" "masterAuth"] [ "services" "redis" "servers" "" "masterAuth" ])
(mkRenamedOptionModule [ "services" "redis" "requirePass"] [ "services" "redis" "servers" "" "requirePass" ])
(mkRenamedOptionModule [ "services" "redis" "requirePassFile"] [ "services" "redis" "servers" "" "requirePassFile" ])
(mkRenamedOptionModule [ "services" "redis" "appendOnly"] [ "services" "redis" "servers" "" "appendOnly" ])
(mkRenamedOptionModule [ "services" "redis" "appendFsync"] [ "services" "redis" "servers" "" "appendFsync" ])
(mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan"] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ])
(mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen"] [ "services" "redis" "servers" "" "slowLogMaxLen" ])
(mkRenamedOptionModule [ "services" "redis" "settings"] [ "services" "redis" "servers" "" "settings" ])
];
###### interface
@ -32,18 +54,6 @@ in {
options = {
services.redis = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the Redis server. Note that the NixOS module for
Redis disables kernel support for Transparent Huge Pages (THP),
because this features causes major performance problems for Redis,
e.g. (https://redis.io/topics/latency).
'';
};
package = mkOption {
type = types.package;
default = pkgs.redis;
@ -51,176 +61,226 @@ in {
description = "Which Redis derivation to use.";
};
port = mkOption {
type = types.port;
default = 6379;
description = "The port for Redis to listen to.";
};
vmOverCommit = mkOption {
type = types.bool;
default = false;
description = ''
Set vm.overcommit_memory to 1 (Suggested for Background Saving: http://redis.io/topics/faq)
'';
};
vmOverCommit = mkEnableOption ''
setting of vm.overcommit_memory to 1
(Suggested for Background Saving: http://redis.io/topics/faq)
'';
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};
servers = mkOption {
type = with types; attrsOf (submodule ({config, name, ...}@args: {
options = {
enable = mkEnableOption ''
Redis server.
Note that the NixOS module for Redis disables kernel support
for Transparent Huge Pages (THP),
because this features causes major performance problems for Redis,
e.g. (https://redis.io/topics/latency).
'';
user = mkOption {
type = types.str;
default = redisName name;
defaultText = "\"redis\" or \"redis-\${name}\" if name != \"\"";
description = "The username and groupname for redis-server.";
};
bind = mkOption {
type = with types; nullOr str;
default = "127.0.0.1";
description = ''
The IP interface to bind to.
<literal>null</literal> means "all interfaces".
'';
example = "192.0.2.1";
};
port = mkOption {
type = types.port;
default = 6379;
description = "The port for Redis to listen to.";
};
unixSocket = mkOption {
type = with types; nullOr path;
default = null;
description = "The path to the socket to bind to.";
example = "/run/redis/redis.sock";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};
unixSocketPerm = mkOption {
type = types.int;
default = 750;
description = "Change permissions for the socket";
example = 700;
};
bind = mkOption {
type = with types; nullOr str;
default = if name == "" then "127.0.0.1" else null;
defaultText = "127.0.0.1 or null if name != \"\"";
description = ''
The IP interface to bind to.
<literal>null</literal> means "all interfaces".
'';
example = "192.0.2.1";
};
logLevel = mkOption {
type = types.str;
default = "notice"; # debug, verbose, notice, warning
example = "debug";
description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
};
unixSocket = mkOption {
type = with types; nullOr path;
default = "/run/${redisName name}/redis.sock";
defaultText = "\"/run/redis/redis.sock\" or \"/run/redis-\${name}/redis.sock\" if name != \"\"";
description = "The path to the socket to bind to.";
};
logfile = mkOption {
type = types.str;
default = "/dev/null";
description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
example = "/var/log/redis.log";
};
unixSocketPerm = mkOption {
type = types.int;
default = 660;
description = "Change permissions for the socket";
example = 600;
};
syslog = mkOption {
type = types.bool;
default = true;
description = "Enable logging to the system logger.";
};
logLevel = mkOption {
type = types.str;
default = "notice"; # debug, verbose, notice, warning
example = "debug";
description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
};
databases = mkOption {
type = types.int;
default = 16;
description = "Set the number of databases.";
};
logfile = mkOption {
type = types.str;
default = "/dev/null";
description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
example = "/var/log/redis.log";
};
maxclients = mkOption {
type = types.int;
default = 10000;
description = "Set the max number of connected clients at the same time.";
};
syslog = mkOption {
type = types.bool;
default = true;
description = "Enable logging to the system logger.";
};
save = mkOption {
type = with types; listOf (listOf int);
default = [ [900 1] [300 10] [60 10000] ];
description = "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.";
};
databases = mkOption {
type = types.int;
default = 16;
description = "Set the number of databases.";
};
slaveOf = mkOption {
type = with types; nullOr (submodule ({ ... }: {
options = {
ip = mkOption {
type = str;
description = "IP of the Redis master";
example = "192.168.1.100";
maxclients = mkOption {
type = types.int;
default = 10000;
description = "Set the max number of connected clients at the same time.";
};
port = mkOption {
type = port;
description = "port of the Redis master";
default = 6379;
save = mkOption {
type = with types; listOf (listOf int);
default = [ [900 1] [300 10] [60 10000] ];
description = "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.";
};
};
}));
default = null;
description = "IP and port to which this redis instance acts as a slave.";
example = { ip = "192.168.1.100"; port = 6379; };
};
slaveOf = mkOption {
type = with types; nullOr (submodule ({ ... }: {
options = {
ip = mkOption {
type = str;
description = "IP of the Redis master";
example = "192.168.1.100";
};
port = mkOption {
type = port;
description = "port of the Redis master";
default = 6379;
};
};
}));
default = null;
description = "IP and port to which this redis instance acts as a slave.";
example = { ip = "192.168.1.100"; port = 6379; };
};
masterAuth = mkOption {
type = with types; nullOr str;
default = null;
description = ''If the master is password protected (using the requirePass configuration)
it is possible to tell the slave to authenticate before starting the replication synchronization
process, otherwise the master will refuse the slave request.
(STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
};
masterAuth = mkOption {
type = with types; nullOr str;
default = null;
description = ''If the master is password protected (using the requirePass configuration)
it is possible to tell the slave to authenticate before starting the replication synchronization
process, otherwise the master will refuse the slave request.
(STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
};
requirePass = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
Use requirePassFile to store it outside of the nix store in a dedicated file.
'';
example = "letmein!";
};
requirePass = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
Use requirePassFile to store it outside of the nix store in a dedicated file.
'';
example = "letmein!";
};
requirePassFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File with password for the database.";
example = "/run/keys/redis-password";
};
requirePassFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File with password for the database.";
example = "/run/keys/redis-password";
};
appendOnly = mkOption {
type = types.bool;
default = false;
description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
};
appendOnly = mkOption {
type = types.bool;
default = false;
description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
};
appendFsync = mkOption {
type = types.str;
default = "everysec"; # no, always, everysec
description = "How often to fsync the append-only log, options: no, always, everysec.";
};
appendFsync = mkOption {
type = types.str;
default = "everysec"; # no, always, everysec
description = "How often to fsync the append-only log, options: no, always, everysec.";
};
slowLogLogSlowerThan = mkOption {
type = types.int;
default = 10000;
description = "Log queries whose execution take longer than X in milliseconds.";
example = 1000;
};
slowLogLogSlowerThan = mkOption {
type = types.int;
default = 10000;
description = "Log queries whose execution take longer than X in milliseconds.";
example = 1000;
};
slowLogMaxLen = mkOption {
type = types.int;
default = 128;
description = "Maximum number of items to keep in slow log.";
};
slowLogMaxLen = mkOption {
type = types.int;
default = 128;
description = "Maximum number of items to keep in slow log.";
};
settings = mkOption {
type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
settings = mkOption {
# TODO: this should be converted to freeformType
type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
default = {};
description = ''
Redis configuration. Refer to
<link xlink:href="https://redis.io/topics/config"/>
for details on supported values.
'';
example = literalExpression ''
{
loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
}
'';
};
};
config.settings = mkMerge [
{
port = if config.bind == null then 0 else config.port;
daemonize = false;
supervised = "systemd";
loglevel = config.logLevel;
logfile = config.logfile;
syslog-enabled = config.syslog;
databases = config.databases;
maxclients = config.maxclients;
save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
dbfilename = "dump.rdb";
dir = "/var/lib/${redisName name}";
appendOnly = config.appendOnly;
appendfsync = config.appendFsync;
slowlog-log-slower-than = config.slowLogLogSlowerThan;
slowlog-max-len = config.slowLogMaxLen;
}
(mkIf (config.bind != null) { bind = config.bind; })
(mkIf (config.unixSocket != null) {
unixsocket = config.unixSocket;
unixsocketperm = toString config.unixSocketPerm;
})
(mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
(mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
(mkIf (config.requirePass != null) { requirepass = config.requirePass; })
];
}));
description = "Configuration of multiple <literal>redis-server</literal> instances.";
default = {};
description = ''
Redis configuration. Refer to
<link xlink:href="https://redis.io/topics/config"/>
for details on supported values.
'';
example = literalExpression ''
{
loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
}
'';
};
};
@ -229,78 +289,61 @@ in {
###### implementation
config = mkIf config.services.redis.enable {
assertions = [{
assertion = cfg.requirePass != null -> cfg.requirePassFile == null;
message = "You can only set one services.redis.requirePass or services.redis.requirePassFile";
}];
boot.kernel.sysctl = (mkMerge [
config = mkIf (enabledServers != {}) {
assertions = attrValues (mapAttrs (name: conf: {
assertion = conf.requirePass != null -> conf.requirePassFile == null;
message = ''
You can only set one services.redis.servers.${name}.requirePass
or services.redis.servers.${name}.requirePassFile
'';
}) enabledServers);
boot.kernel.sysctl = mkMerge [
{ "vm.nr_hugepages" = "0"; }
( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
]);
];
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
users.users.redis = {
description = "Redis database user";
group = "redis";
isSystemUser = true;
};
users.groups.redis = {};
networking.firewall.allowedTCPPorts = concatMap (conf:
optional conf.openFirewall conf.port
) (attrValues enabledServers);
environment.systemPackages = [ cfg.package ];
services.redis.settings = mkMerge [
{
port = cfg.port;
daemonize = false;
supervised = "systemd";
loglevel = cfg.logLevel;
logfile = cfg.logfile;
syslog-enabled = cfg.syslog;
databases = cfg.databases;
maxclients = cfg.maxclients;
save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
dbfilename = "dump.rdb";
dir = "/var/lib/redis";
appendOnly = cfg.appendOnly;
appendfsync = cfg.appendFsync;
slowlog-log-slower-than = cfg.slowLogLogSlowerThan;
slowlog-max-len = cfg.slowLogMaxLen;
}
(mkIf (cfg.bind != null) { bind = cfg.bind; })
(mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
(mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}"; })
(mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
(mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
];
users.users = mapAttrs' (name: conf: nameValuePair (redisName name) {
description = "System user for the redis-server instance ${name}";
isSystemUser = true;
group = redisName name;
}) enabledServers;
users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) {
}) enabledServers;
systemd.services.redis = {
description = "Redis Server";
systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) {
description = "Redis Server - ${redisName name}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
install -m 600 ${redisConfig} /run/redis/redis.conf
'' + optionalString (cfg.requirePassFile != null) ''
password=$(cat ${escapeShellArg cfg.requirePassFile})
echo "requirePass $password" >> /run/redis/redis.conf
'';
serviceConfig = {
ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
ExecStart = "${cfg.package}/bin/redis-server /run/${redisName name}/redis.conf";
ExecStartPre = [("+"+pkgs.writeShellScript "${redisName name}-credentials" (''
install -o '${conf.user}' -m 600 ${redisConfig conf.settings} /run/${redisName name}/redis.conf
'' + optionalString (conf.requirePassFile != null) ''
{
printf requirePass' '
cat ${escapeShellArg conf.requirePassFile}
} >>/run/${redisName name}/redis.conf
'')
)];
Type = "notify";
# User and group
User = "redis";
Group = "redis";
User = conf.user;
Group = conf.user;
# Runtime directory and mode
RuntimeDirectory = "redis";
RuntimeDirectory = redisName name;
RuntimeDirectoryMode = "0750";
# State directory and mode
StateDirectory = "redis";
StateDirectory = redisName name;
StateDirectoryMode = "0700";
# Access write directories
UMask = "0077";
@ -309,7 +352,7 @@ in {
# Security
NoNewPrivileges = true;
# Process Properties
LimitNOFILE = "${toString ulimitNofile}";
LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
@ -322,7 +365,9 @@ in {
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictAddressFamilies =
optionals (conf.bind != null) ["AF_INET" "AF_INET6"] ++
optional (conf.unixSocket != null) "AF_UNIX";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
@ -333,6 +378,7 @@ in {
SystemCallArchitectures = "native";
SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
};
};
}) enabledServers;
};
}

Loading…
Cancel
Save