1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.redis;
7
8 ulimitNofile = cfg.maxclients + 32;
9
10 mkValueString = value:
11 if value == true then "yes"
12 else if value == false then "no"
13 else generators.mkValueStringDefault { } value;
14
15 redisConfig = pkgs.writeText "redis.conf" (generators.toKeyValue {
16 listsAsDuplicateKeys = true;
17 mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
18 } cfg.settings);
19
20in {
21 imports = [
22 (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.")
23 (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.")
24 (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
25 (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
26 (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
27 (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
28 ];
29
30 ###### interface
31
32 options = {
33
34 services.redis = {
35
36 enable = mkOption {
37 type = types.bool;
38 default = false;
39 description = ''
40 Whether to enable the Redis server. Note that the NixOS module for
41 Redis disables kernel support for Transparent Huge Pages (THP),
42 because this features causes major performance problems for Redis,
43 e.g. (https://redis.io/topics/latency).
44 '';
45 };
46
47 package = mkOption {
48 type = types.package;
49 default = pkgs.redis;
50 defaultText = literalExpression "pkgs.redis";
51 description = "Which Redis derivation to use.";
52 };
53
54 port = mkOption {
55 type = types.port;
56 default = 6379;
57 description = "The port for Redis to listen to.";
58 };
59
60 vmOverCommit = mkOption {
61 type = types.bool;
62 default = false;
63 description = ''
64 Set vm.overcommit_memory to 1 (Suggested for Background Saving: http://redis.io/topics/faq)
65 '';
66 };
67
68 openFirewall = mkOption {
69 type = types.bool;
70 default = false;
71 description = ''
72 Whether to open ports in the firewall for the server.
73 '';
74 };
75
76 bind = mkOption {
77 type = with types; nullOr str;
78 default = "127.0.0.1";
79 description = ''
80 The IP interface to bind to.
81 <literal>null</literal> means "all interfaces".
82 '';
83 example = "192.0.2.1";
84 };
85
86 unixSocket = mkOption {
87 type = with types; nullOr path;
88 default = null;
89 description = "The path to the socket to bind to.";
90 example = "/run/redis/redis.sock";
91 };
92
93 unixSocketPerm = mkOption {
94 type = types.int;
95 default = 750;
96 description = "Change permissions for the socket";
97 example = 700;
98 };
99
100 logLevel = mkOption {
101 type = types.str;
102 default = "notice"; # debug, verbose, notice, warning
103 example = "debug";
104 description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
105 };
106
107 logfile = mkOption {
108 type = types.str;
109 default = "/dev/null";
110 description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
111 example = "/var/log/redis.log";
112 };
113
114 syslog = mkOption {
115 type = types.bool;
116 default = true;
117 description = "Enable logging to the system logger.";
118 };
119
120 databases = mkOption {
121 type = types.int;
122 default = 16;
123 description = "Set the number of databases.";
124 };
125
126 maxclients = mkOption {
127 type = types.int;
128 default = 10000;
129 description = "Set the max number of connected clients at the same time.";
130 };
131
132 save = mkOption {
133 type = with types; listOf (listOf int);
134 default = [ [900 1] [300 10] [60 10000] ];
135 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.";
136 };
137
138 slaveOf = mkOption {
139 type = with types; nullOr (submodule ({ ... }: {
140 options = {
141 ip = mkOption {
142 type = str;
143 description = "IP of the Redis master";
144 example = "192.168.1.100";
145 };
146
147 port = mkOption {
148 type = port;
149 description = "port of the Redis master";
150 default = 6379;
151 };
152 };
153 }));
154
155 default = null;
156 description = "IP and port to which this redis instance acts as a slave.";
157 example = { ip = "192.168.1.100"; port = 6379; };
158 };
159
160 masterAuth = mkOption {
161 type = with types; nullOr str;
162 default = null;
163 description = ''If the master is password protected (using the requirePass configuration)
164 it is possible to tell the slave to authenticate before starting the replication synchronization
165 process, otherwise the master will refuse the slave request.
166 (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
167 };
168
169 requirePass = mkOption {
170 type = with types; nullOr str;
171 default = null;
172 description = ''
173 Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
174 Use requirePassFile to store it outside of the nix store in a dedicated file.
175 '';
176 example = "letmein!";
177 };
178
179 requirePassFile = mkOption {
180 type = with types; nullOr path;
181 default = null;
182 description = "File with password for the database.";
183 example = "/run/keys/redis-password";
184 };
185
186 appendOnly = mkOption {
187 type = types.bool;
188 default = false;
189 description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
190 };
191
192 appendFsync = mkOption {
193 type = types.str;
194 default = "everysec"; # no, always, everysec
195 description = "How often to fsync the append-only log, options: no, always, everysec.";
196 };
197
198 slowLogLogSlowerThan = mkOption {
199 type = types.int;
200 default = 10000;
201 description = "Log queries whose execution take longer than X in milliseconds.";
202 example = 1000;
203 };
204
205 slowLogMaxLen = mkOption {
206 type = types.int;
207 default = 128;
208 description = "Maximum number of items to keep in slow log.";
209 };
210
211 settings = mkOption {
212 type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
213 default = {};
214 description = ''
215 Redis configuration. Refer to
216 <link xlink:href="https://redis.io/topics/config"/>
217 for details on supported values.
218 '';
219 example = literalExpression ''
220 {
221 loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
222 }
223 '';
224 };
225 };
226
227 };
228
229
230 ###### implementation
231
232 config = mkIf config.services.redis.enable {
233 assertions = [{
234 assertion = cfg.requirePass != null -> cfg.requirePassFile == null;
235 message = "You can only set one services.redis.requirePass or services.redis.requirePassFile";
236 }];
237 boot.kernel.sysctl = (mkMerge [
238 { "vm.nr_hugepages" = "0"; }
239 ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
240 ]);
241
242 networking.firewall = mkIf cfg.openFirewall {
243 allowedTCPPorts = [ cfg.port ];
244 };
245
246 users.users.redis = {
247 description = "Redis database user";
248 group = "redis";
249 isSystemUser = true;
250 };
251 users.groups.redis = {};
252
253 environment.systemPackages = [ cfg.package ];
254
255 services.redis.settings = mkMerge [
256 {
257 port = cfg.port;
258 daemonize = false;
259 supervised = "systemd";
260 loglevel = cfg.logLevel;
261 logfile = cfg.logfile;
262 syslog-enabled = cfg.syslog;
263 databases = cfg.databases;
264 maxclients = cfg.maxclients;
265 save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
266 dbfilename = "dump.rdb";
267 dir = "/var/lib/redis";
268 appendOnly = cfg.appendOnly;
269 appendfsync = cfg.appendFsync;
270 slowlog-log-slower-than = cfg.slowLogLogSlowerThan;
271 slowlog-max-len = cfg.slowLogMaxLen;
272 }
273 (mkIf (cfg.bind != null) { bind = cfg.bind; })
274 (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
275 (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}"; })
276 (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
277 (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
278 ];
279
280 systemd.services.redis = {
281 description = "Redis Server";
282
283 wantedBy = [ "multi-user.target" ];
284 after = [ "network.target" ];
285
286 preStart = ''
287 install -m 600 ${redisConfig} /run/redis/redis.conf
288 '' + optionalString (cfg.requirePassFile != null) ''
289 password=$(cat ${escapeShellArg cfg.requirePassFile})
290 echo "requirePass $password" >> /run/redis/redis.conf
291 '';
292
293 serviceConfig = {
294 ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
295 Type = "notify";
296 # User and group
297 User = "redis";
298 Group = "redis";
299 # Runtime directory and mode
300 RuntimeDirectory = "redis";
301 RuntimeDirectoryMode = "0750";
302 # State directory and mode
303 StateDirectory = "redis";
304 StateDirectoryMode = "0700";
305 # Access write directories
306 UMask = "0077";
307 # Capabilities
308 CapabilityBoundingSet = "";
309 # Security
310 NoNewPrivileges = true;
311 # Process Properties
312 LimitNOFILE = "${toString ulimitNofile}";
313 # Sandboxing
314 ProtectSystem = "strict";
315 ProtectHome = true;
316 PrivateTmp = true;
317 PrivateDevices = true;
318 PrivateUsers = true;
319 ProtectClock = true;
320 ProtectHostname = true;
321 ProtectKernelLogs = true;
322 ProtectKernelModules = true;
323 ProtectKernelTunables = true;
324 ProtectControlGroups = true;
325 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
326 RestrictNamespaces = true;
327 LockPersonality = true;
328 MemoryDenyWriteExecute = true;
329 RestrictRealtime = true;
330 RestrictSUIDSGID = true;
331 PrivateMounts = true;
332 # System Call Filtering
333 SystemCallArchitectures = "native";
334 SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
335 };
336 };
337 };
338}