From 1a05178b43c63b23d4ba8bfdc170e41ab54a931b Mon Sep 17 00:00:00 2001 From: Ashley Hooper Date: Thu, 1 Jan 2026 11:34:00 +1300 Subject: [PATCH 1/2] nixos/redis: add support for more auth options - Add Redis auth options that are important for running a secure Redis infrastructure - `masteruser` and `masterauth` for authentication between replicas and master - `sentinel auth-user` and `sentinel auth-pass` to provide credentials that a Sentinel instance should use to authenticate to the Redis instance it is monitoring - Add assertions to catch configuration issues before build/deployment Signed-off-by: Ashley Hooper --- nixos/modules/services/databases/redis.nix | 94 +++++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix index 157a6c6b84e3..c9aa42557a11 100644 --- a/nixos/modules/services/databases/redis.nix +++ b/nixos/modules/services/databases/redis.nix @@ -335,7 +335,23 @@ in 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)''; + (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE) + ''; + }; + + masterAuthFile = lib.mkOption { + type = with types; nullOr path; + default = null; + description = "File with password for the master user."; + example = "/run/keys/redis-master-password"; + }; + + masterUser = lib.mkOption { + type = with types; nullOr str; + default = null; + description = '' + If the master is password protected via ACLs this option can be used to specify + the Redis user that is used by replicas.''; }; requirePass = lib.mkOption { @@ -355,6 +371,25 @@ in example = "/run/keys/redis-password"; }; + sentinelAuthUser = lib.mkOption { + type = with types; nullOr str; + default = null; + description = "The username to use to monitor a master from Sentinel."; + }; + + sentinelAuthPassFile = lib.mkOption { + type = with types; nullOr path; + default = null; + description = "File with password for connecting to other Sentinel instances."; + example = "/run/keys/sentinel-password"; + }; + + sentinelMasterName = lib.mkOption { + type = with types; nullOr str; + default = null; + description = "The master name of the Redis master that Sentinel will monitor."; + }; + appendOnly = lib.mkOption { type = types.bool; default = false; @@ -452,14 +487,38 @@ in config = lib.mkIf (enabledServers != { }) { - assertions = lib.attrValues ( - lib.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 + assertions = lib.concatLists ( + lib.mapAttrsToList (name: conf: [ + { + assertion = conf.requirePass != null -> conf.requirePassFile == null; + message = '' + You can only set one of services.redis.servers.${name}.requirePass + or services.redis.servers.${name}.requirePassFile + ''; + } + { + assertion = conf.masterAuth != null -> conf.masterAuthFile == null; + message = '' + You can only set one of services.redis.servers.${name}.masterAuth + or services.redis.servers.${name}.masterAuthFile + ''; + } + { + assertion = conf.masterUser != null -> (conf.masterAuth != null || conf.masterAuthFile != null); + message = '' + If using services.redis.servers.${name}.masterUser, either + services.redis.servers.${name}.masterAuthFile or + services.redis.servers.${name}.masterAuth must be provided + ''; + } + { + assertion = conf.sentinelAuthPassFile != null -> conf.sentinelMasterName != null; + message = '' + For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName + must be specified + ''; + } + ]) enabledServers ); boot.kernel.sysctl = lib.mkIf cfg.vmOverCommit { @@ -515,10 +574,19 @@ in fi echo 'include "${redisConfStore}"' > "${redisConfRun}" ${lib.optionalString (conf.requirePassFile != null) '' - { - echo -n "requirepass " - cat ${lib.escapeShellArg conf.requirePassFile} - } >> "${redisConfRun}" + echo "requirepass $(cat ${lib.escapeShellArg conf.requirePassFile})" >> "${redisConfRun}" + ''} + ${lib.optionalString (conf.masterUser != null) '' + echo "masteruser ${conf.masterUser}" >> "${redisConfRun}" + ''} + ${lib.optionalString (conf.masterAuthFile != null) '' + echo "masterauth $(cat ${lib.escapeShellArg conf.masterAuthFile})" >> "${redisConfRun}" + ''} + ${lib.optionalString (conf.sentinelAuthUser != null) '' + echo "sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}" >> "${redisConfRun}" + ''} + ${lib.optionalString (conf.sentinelAuthPassFile != null) '' + echo "sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})" >> "${redisConfRun}" ''} '' ); From 21a5656772f35292c98af4960e8c022a8c94ca2d Mon Sep 17 00:00:00 2001 From: Ashley Hooper Date: Mon, 26 Jan 2026 17:36:40 +1300 Subject: [PATCH 2/2] nixos/redis: rework Sentinel options - Add sentinelMasterHost, sentinelMasterPort, sentinelMasterQuorum options - Add special handling for mutable state in /var/lib/redis-xxx Redis/Valkey Sentinel persists dynamic cluster state by rewriting its configuration file at runtime. This behaviour cannot be disabled. Add conditional preStart logic to avoid overwriting Sentinel-owned configuration while preserving NixOS-managed defaults. This introduces a necessary, documented deviation from strict declarative semantics. --- nixos/modules/services/databases/redis.nix | 87 +++++++++++++++++++--- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix index c9aa42557a11..d55658bb25b2 100644 --- a/nixos/modules/services/databases/redis.nix +++ b/nixos/modules/services/databases/redis.nix @@ -371,12 +371,6 @@ in example = "/run/keys/redis-password"; }; - sentinelAuthUser = lib.mkOption { - type = with types; nullOr str; - default = null; - description = "The username to use to monitor a master from Sentinel."; - }; - sentinelAuthPassFile = lib.mkOption { type = with types; nullOr path; default = null; @@ -384,12 +378,36 @@ in example = "/run/keys/sentinel-password"; }; + sentinelAuthUser = lib.mkOption { + type = with types; nullOr str; + default = null; + description = "The username to use to monitor a master from Sentinel."; + }; + + sentinelMasterHost = lib.mkOption { + type = with types; nullOr str; + default = null; + description = "The IP address (recommended) or hostname of the Redis master that Sentinel will monitor."; + }; + sentinelMasterName = lib.mkOption { type = with types; nullOr str; default = null; description = "The master name of the Redis master that Sentinel will monitor."; }; + sentinelMasterPort = lib.mkOption { + type = with types; nullOr int; + default = null; + description = "The TCP port of the Redis master that Sentinel will monitor."; + }; + + sentinelMasterQuorum = lib.mkOption { + type = with types; nullOr int; + default = null; + description = "The Sentinel quorum (minimum number of Sentinel nodes online for failover)"; + }; + appendOnly = lib.mkOption { type = types.bool; default = false; @@ -511,11 +529,28 @@ in services.redis.servers.${name}.masterAuth must be provided ''; } + { + assertion = + conf.sentinelMasterName != null + -> ( + conf.sentinelMasterHost != null + && conf.sentinelMasterPort != null + && conf.sentinelMasterQuorum != null + ); + message = '' + For Sentinel, + services.redis.servers.${name}.sentinelMasterName, + services.redis.servers.${name}.sentinelMasterHost, + services.redis.servers.${name}.sentinelMasterPort, + and services.redis.servers.${name}.sentinelMasterQuorum + must all be provided + ''; + } { assertion = conf.sentinelAuthPassFile != null -> conf.sentinelMasterName != null; message = '' - For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName - must be specified + For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName, + must be provided ''; } ]) enabledServers @@ -557,6 +592,17 @@ in ExecStart = "${cfg.package}/bin/${ cfg.package.serverBin or "redis-server" } /var/lib/${redisName name}/redis.conf ${lib.escapeShellArgs conf.extraParams}"; + + # NOTE: Redis/Valkey Sentinel persists dynamic cluster state by rewriting its + # configuration file at runtime (redis.conf). This includes monitors, + # authentication credentials, and failover metadata, and this behaviour + # cannot be disabled. + # As a result, a fully declarative configuration is not possible for + # Sentinel-managed options. The preStart logic below appends sentinel + # configuration only if it is not already present, in order to avoid + # overwriting state that is owned and maintained by Sentinel itself. + # This is an intentional deviation from strict declarative semantics and + # is required for correct Sentinel operation. ExecStartPre = "+" + pkgs.writeShellScript "${redisName name}-prep-conf" ( @@ -582,11 +628,32 @@ in ${lib.optionalString (conf.masterAuthFile != null) '' echo "masterauth $(cat ${lib.escapeShellArg conf.masterAuthFile})" >> "${redisConfRun}" ''} + ${lib.optionalString (conf.sentinelMasterHost != null) '' + sentinel_monitor_line="sentinel monitor ${conf.sentinelMasterName} ${conf.sentinelMasterHost} ${toString conf.sentinelMasterPort} ${toString conf.sentinelMasterQuorum}" + if grep -qE "^sentinel monitor ${conf.sentinelMasterName}\b" "${redisConfVar}"; then + sed -i \ + "s|^sentinel monitor ${conf.sentinelMasterName}\b.*|$sentinel_monitor_line|" "${redisConfVar}" + else + echo "$sentinel_monitor_line" >> "${redisConfVar}" + fi + ''} ${lib.optionalString (conf.sentinelAuthUser != null) '' - echo "sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}" >> "${redisConfRun}" + sentinel_auth_user_line="sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}" + if grep -qE "^sentinel auth-user ${conf.sentinelMasterName}\b" "${redisConfVar}"; then + sed -i \ + "s|^sentinel auth-user ${conf.sentinelMasterName}\b.*|$sentinel_auth_user_line|" "${redisConfVar}" + else + echo "$sentinel_auth_user_line" >> "${redisConfVar}" + fi ''} ${lib.optionalString (conf.sentinelAuthPassFile != null) '' - echo "sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})" >> "${redisConfRun}" + sentinel_auth_pass_line="sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})" + if grep -qE "^sentinel auth-pass ${conf.sentinelMasterName}\b" "${redisConfVar}"; then + sed -i \ + "s|^sentinel auth-pass ${conf.sentinelMasterName}\b.*|$sentinel_auth_pass_line|" "${redisConfVar}" + else + echo "$sentinel_auth_pass_line" >> "${redisConfVar}" + fi ''} '' );