Skip to content

Commit b3a1d52

Browse files
nixos/redis: add support for more auth options (#475735)
2 parents 0787c37 + 21a5656 commit b3a1d52

1 file changed

Lines changed: 148 additions & 13 deletions

File tree

nixos/modules/services/databases/redis.nix

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,23 @@ in
335335
If the master is password protected (using the requirePass configuration)
336336
it is possible to tell the slave to authenticate before starting the replication synchronization
337337
process, otherwise the master will refuse the slave request.
338-
(STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
338+
(STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)
339+
'';
340+
};
341+
342+
masterAuthFile = lib.mkOption {
343+
type = with types; nullOr path;
344+
default = null;
345+
description = "File with password for the master user.";
346+
example = "/run/keys/redis-master-password";
347+
};
348+
349+
masterUser = lib.mkOption {
350+
type = with types; nullOr str;
351+
default = null;
352+
description = ''
353+
If the master is password protected via ACLs this option can be used to specify
354+
the Redis user that is used by replicas.'';
339355
};
340356

341357
requirePass = lib.mkOption {
@@ -355,6 +371,43 @@ in
355371
example = "/run/keys/redis-password";
356372
};
357373

374+
sentinelAuthPassFile = lib.mkOption {
375+
type = with types; nullOr path;
376+
default = null;
377+
description = "File with password for connecting to other Sentinel instances.";
378+
example = "/run/keys/sentinel-password";
379+
};
380+
381+
sentinelAuthUser = lib.mkOption {
382+
type = with types; nullOr str;
383+
default = null;
384+
description = "The username to use to monitor a master from Sentinel.";
385+
};
386+
387+
sentinelMasterHost = lib.mkOption {
388+
type = with types; nullOr str;
389+
default = null;
390+
description = "The IP address (recommended) or hostname of the Redis master that Sentinel will monitor.";
391+
};
392+
393+
sentinelMasterName = lib.mkOption {
394+
type = with types; nullOr str;
395+
default = null;
396+
description = "The master name of the Redis master that Sentinel will monitor.";
397+
};
398+
399+
sentinelMasterPort = lib.mkOption {
400+
type = with types; nullOr int;
401+
default = null;
402+
description = "The TCP port of the Redis master that Sentinel will monitor.";
403+
};
404+
405+
sentinelMasterQuorum = lib.mkOption {
406+
type = with types; nullOr int;
407+
default = null;
408+
description = "The Sentinel quorum (minimum number of Sentinel nodes online for failover)";
409+
};
410+
358411
appendOnly = lib.mkOption {
359412
type = types.bool;
360413
default = false;
@@ -452,14 +505,55 @@ in
452505

453506
config = lib.mkIf (enabledServers != { }) {
454507

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
508+
assertions = lib.concatLists (
509+
lib.mapAttrsToList (name: conf: [
510+
{
511+
assertion = conf.requirePass != null -> conf.requirePassFile == null;
512+
message = ''
513+
You can only set one of services.redis.servers.${name}.requirePass
514+
or services.redis.servers.${name}.requirePassFile
515+
'';
516+
}
517+
{
518+
assertion = conf.masterAuth != null -> conf.masterAuthFile == null;
519+
message = ''
520+
You can only set one of services.redis.servers.${name}.masterAuth
521+
or services.redis.servers.${name}.masterAuthFile
522+
'';
523+
}
524+
{
525+
assertion = conf.masterUser != null -> (conf.masterAuth != null || conf.masterAuthFile != null);
526+
message = ''
527+
If using services.redis.servers.${name}.masterUser, either
528+
services.redis.servers.${name}.masterAuthFile or
529+
services.redis.servers.${name}.masterAuth must be provided
530+
'';
531+
}
532+
{
533+
assertion =
534+
conf.sentinelMasterName != null
535+
-> (
536+
conf.sentinelMasterHost != null
537+
&& conf.sentinelMasterPort != null
538+
&& conf.sentinelMasterQuorum != null
539+
);
540+
message = ''
541+
For Sentinel,
542+
services.redis.servers.${name}.sentinelMasterName,
543+
services.redis.servers.${name}.sentinelMasterHost,
544+
services.redis.servers.${name}.sentinelMasterPort,
545+
and services.redis.servers.${name}.sentinelMasterQuorum
546+
must all be provided
547+
'';
548+
}
549+
{
550+
assertion = conf.sentinelAuthPassFile != null -> conf.sentinelMasterName != null;
551+
message = ''
552+
For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName,
553+
must be provided
554+
'';
555+
}
556+
]) enabledServers
463557
);
464558

465559
boot.kernel.sysctl = lib.mkIf cfg.vmOverCommit {
@@ -498,6 +592,17 @@ in
498592
ExecStart = "${cfg.package}/bin/${
499593
cfg.package.serverBin or "redis-server"
500594
} /var/lib/${redisName name}/redis.conf ${lib.escapeShellArgs conf.extraParams}";
595+
596+
# NOTE: Redis/Valkey Sentinel persists dynamic cluster state by rewriting its
597+
# configuration file at runtime (redis.conf). This includes monitors,
598+
# authentication credentials, and failover metadata, and this behaviour
599+
# cannot be disabled.
600+
# As a result, a fully declarative configuration is not possible for
601+
# Sentinel-managed options. The preStart logic below appends sentinel
602+
# configuration only if it is not already present, in order to avoid
603+
# overwriting state that is owned and maintained by Sentinel itself.
604+
# This is an intentional deviation from strict declarative semantics and
605+
# is required for correct Sentinel operation.
501606
ExecStartPre =
502607
"+"
503608
+ pkgs.writeShellScript "${redisName name}-prep-conf" (
@@ -515,10 +620,40 @@ in
515620
fi
516621
echo 'include "${redisConfStore}"' > "${redisConfRun}"
517622
${lib.optionalString (conf.requirePassFile != null) ''
518-
{
519-
echo -n "requirepass "
520-
cat ${lib.escapeShellArg conf.requirePassFile}
521-
} >> "${redisConfRun}"
623+
echo "requirepass $(cat ${lib.escapeShellArg conf.requirePassFile})" >> "${redisConfRun}"
624+
''}
625+
${lib.optionalString (conf.masterUser != null) ''
626+
echo "masteruser ${conf.masterUser}" >> "${redisConfRun}"
627+
''}
628+
${lib.optionalString (conf.masterAuthFile != null) ''
629+
echo "masterauth $(cat ${lib.escapeShellArg conf.masterAuthFile})" >> "${redisConfRun}"
630+
''}
631+
${lib.optionalString (conf.sentinelMasterHost != null) ''
632+
sentinel_monitor_line="sentinel monitor ${conf.sentinelMasterName} ${conf.sentinelMasterHost} ${toString conf.sentinelMasterPort} ${toString conf.sentinelMasterQuorum}"
633+
if grep -qE "^sentinel monitor ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
634+
sed -i \
635+
"s|^sentinel monitor ${conf.sentinelMasterName}\b.*|$sentinel_monitor_line|" "${redisConfVar}"
636+
else
637+
echo "$sentinel_monitor_line" >> "${redisConfVar}"
638+
fi
639+
''}
640+
${lib.optionalString (conf.sentinelAuthUser != null) ''
641+
sentinel_auth_user_line="sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}"
642+
if grep -qE "^sentinel auth-user ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
643+
sed -i \
644+
"s|^sentinel auth-user ${conf.sentinelMasterName}\b.*|$sentinel_auth_user_line|" "${redisConfVar}"
645+
else
646+
echo "$sentinel_auth_user_line" >> "${redisConfVar}"
647+
fi
648+
''}
649+
${lib.optionalString (conf.sentinelAuthPassFile != null) ''
650+
sentinel_auth_pass_line="sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})"
651+
if grep -qE "^sentinel auth-pass ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
652+
sed -i \
653+
"s|^sentinel auth-pass ${conf.sentinelMasterName}\b.*|$sentinel_auth_pass_line|" "${redisConfVar}"
654+
else
655+
echo "$sentinel_auth_pass_line" >> "${redisConfVar}"
656+
fi
522657
''}
523658
''
524659
);

0 commit comments

Comments
 (0)