Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| tcpKeepAlive={bool} | `TcpKeepAlive` | `true` | Enables TCP keep-alive when appropriate (endpoint- and platform-dependent) |
| name={string} | `ClientName` | `null` | Identification for the connection within redis |
| password={string} | `Password` | `null` | Password for the redis server |
| sentinelPassword={string} | `SentinelPassword` | `null` | Optional password to authenticate with Sentinel servers (falls back to `password` if not provided) |
| user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) |
| sentinelUser={string} | `SentinelUser` | `null` | Optional username to authenticate with Sentinel servers (falls back to `user` if not provided) |
| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service |
Expand Down
43 changes: 38 additions & 5 deletions src/StackExchange.Redis/ConfigurationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Security;
Expand All @@ -14,8 +13,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using RESPite;
using RESPite.Buffers;
using StackExchange.Redis.Configuration;

namespace StackExchange.Redis
Expand Down Expand Up @@ -98,7 +95,9 @@ internal const string
KeepAlive = "keepAlive",
ClientName = "name",
User = "user",
SentinelUser = "sentinelUser",
Password = "password",
SentinelPassword = "sentinelPassword",
PreserveAsyncOrder = "preserveAsyncOrder",
Proxy = "proxy",
ResolveDns = "resolveDns",
Expand Down Expand Up @@ -133,7 +132,9 @@ internal const string
HighPrioritySocketThreads,
KeepAlive,
User,
SentinelUser,
Password,
SentinelPassword,
PreserveAsyncOrder,
Proxy,
ResolveDns,
Expand Down Expand Up @@ -209,7 +210,7 @@ private enum OptionFlags : ulong

private OptionFlags optionFlags;

private string? tieBreaker, sslHost, configChannel, user, password;
private string? tieBreaker, sslHost, configChannel, user, sentinelUser, password, sentinelPassword;
Comment thread
doosterkamp marked this conversation as resolved.

private TimeSpan heartbeatInterval;

Expand Down Expand Up @@ -738,6 +739,16 @@ public string? User
set => user = value;
}

/// <summary>
/// The username to use to authenticate with Sentinel servers, only when different from the Redis server password (optional).
/// If not specified, <see cref="User"/> is used when communicating with Sentinels.
/// </summary>
public string? SentinelUser
{
get => sentinelUser ?? user ?? Defaults.User;
set => sentinelUser = value;
}

/// <summary>
/// The password to use to authenticate with the server.
/// </summary>
Expand All @@ -747,6 +758,16 @@ public string? Password
set => password = value;
}

/// <summary>
/// The password to use to authenticate with Sentinel servers, only when different from the Redis server password (optional).
/// If not specified, <see cref="Password"/> is used when communicating with Sentinels.
/// </summary>
public string? SentinelPassword
{
get => sentinelPassword ?? password ?? Defaults.Password;
set => sentinelPassword = value;
}

/// <summary>
/// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order.
/// </summary>
Expand Down Expand Up @@ -932,7 +953,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
public ConfigurationOptions Clone() => new ConfigurationOptions
{
defaultOptions = defaultOptions,
optionFlags = this.optionFlags,
optionFlags = optionFlags,
ClientName = ClientName,
ServiceName = ServiceName,
keepAlive = keepAlive,
Expand All @@ -941,7 +962,9 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
defaultVersion = defaultVersion,
connectTimeout = connectTimeout,
user = user,
sentinelUser = sentinelUser,
password = password,
sentinelPassword = sentinelPassword,
tieBreaker = tieBreaker,
sslHost = sslHost,
configChannel = configChannel,
Expand Down Expand Up @@ -1040,7 +1063,9 @@ public string ToString(bool includePassword)
Append(sb, OptionKeys.Version, defaultVersion);
Append(sb, OptionKeys.ConnectTimeout, OptionFlags.ConnectTimeoutHasValue, in connectTimeout);
Append(sb, OptionKeys.User, user);
Append(sb, OptionKeys.SentinelUser, sentinelUser);
Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(password)) ? password : "*****");
Append(sb, OptionKeys.SentinelPassword, (includePassword || string.IsNullOrEmpty(sentinelPassword)) ? sentinelPassword : "*****");
Append(sb, OptionKeys.TieBreaker, tieBreaker);
Append(sb, OptionKeys.Ssl, OptionFlags.SslHasValue, OptionFlags.SslValue);
if (HasValue(OptionFlags.SslProtocolsHasValue)) Append(sb, OptionKeys.SslProtocols, sslProtocols.ToString().Replace(',', '|'));
Expand Down Expand Up @@ -1177,6 +1202,8 @@ private void Clear()
#if DEBUG
OutputLog = null;
#endif
sentinelUser = null;
sentinelPassword = null;
}

object ICloneable.Clone() => Clone();
Expand Down Expand Up @@ -1261,9 +1288,15 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
case OptionKeys.User:
user = value;
break;
case OptionKeys.SentinelUser:
SentinelUser = value;
break;
case OptionKeys.Password:
password = value;
break;
case OptionKeys.SentinelPassword:
SentinelPassword = value;
break;
case OptionKeys.TieBreaker:
TieBreaker = value;
break;
Expand Down
4 changes: 3 additions & 1 deletion src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public partial class ConnectionMultiplexer
/// <param name="log">The <see cref="ILogger"/> to log to, if any.</param>
internal void InitializeSentinel(ILogger? log)
{
if (ServerSelectionStrategy.ServerType != ServerType.Sentinel)
if (!_isSentinel)
{
return;
}
Expand Down Expand Up @@ -144,6 +144,7 @@ public static Task<ConnectionMultiplexer> SentinelConnectAsync(ConfigurationOpti
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter? log = null)
{
// Sentinel credentials are selected during handshake based on ServerType
var sentinelConnection = SentinelConnect(configuration, log);

var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log);
Expand All @@ -161,6 +162,7 @@ private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions
/// <param name="writer">The <see cref="TextWriter"/> to log to.</param>
private static async Task<ConnectionMultiplexer> SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter? writer = null)
{
// Sentinel credentials are selected during handshake based on ServerType
var sentinelConnection = await SentinelConnectAsync(configuration, writer).ForAwait();

var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, writer);
Expand Down
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/ConnectionMultiplexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex
internal bool IsDisposed => _isDisposed;
internal ILogger<ConnectionMultiplexer>? Logger { get; }

private readonly bool _isSentinel;

internal CommandMap CommandMap { get; }
internal EndPointCollection EndPoints { get; }
internal ConfigurationOptions RawConfig { get; }
Expand Down Expand Up @@ -134,6 +136,8 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se
EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl);
Logger = configuration.LoggerFactory?.CreateLogger<ConnectionMultiplexer>();

_isSentinel = serverType == ServerType.Sentinel;

var map = CommandMap = configuration.GetCommandMap(serverType);
if (!string.IsNullOrWhiteSpace(configuration.Password) && !configuration.TryResp3()) // RESP3 doesn't need AUTH (can issue as part of HELLO)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#nullable enable

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should all just be in PublicAPI.Unshipped.txt - it isn't feature-specific

StackExchange.Redis.ConfigurationOptions.SentinelUser.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelUser.set -> void
StackExchange.Redis.ConfigurationOptions.SentinelPassword.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelPassword.set -> void
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ StackExchange.Redis.ConfigurationOptions.ResolveDns.get -> bool
StackExchange.Redis.ConfigurationOptions.ResolveDns.set -> void
StackExchange.Redis.ConfigurationOptions.ResponseTimeout.get -> int
StackExchange.Redis.ConfigurationOptions.ResponseTimeout.set -> void
StackExchange.Redis.ConfigurationOptions.SentinelUser.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelUser.set -> void
StackExchange.Redis.ConfigurationOptions.SentinelPassword.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelPassword.set -> void
StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string?
StackExchange.Redis.ConfigurationOptions.ServiceName.set -> void
StackExchange.Redis.ConfigurationOptions.SetClientLibrary.get -> bool
Expand Down
10 changes: 6 additions & 4 deletions src/StackExchange.Redis/ServerEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -957,13 +957,15 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
Multiplexer.Trace("No connection!?");
return;
}

Message msg;
// Note that we need "" (not null) for password in the case of 'nopass' logins
var config = Multiplexer.RawConfig;
string? user = config.User;
string password = config.Password ?? "";
var isSentinelConnection = Multiplexer.ServerSelectionStrategy.ServerType == ServerType.Sentinel;

@mgravell mgravell Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not be checking something like Multiplexer.IsSentinel, i.e. that bool we just added? Alternatively: if the code shown is valid: we possibly don't need the explicit bool, and a property like

internal bool IsSential => ServerSelectionStrategy.ServerType is ServerType.Sentinel;

is fine, without the explicit bool ? In either case: we should be consistent in the decision process.

var user = isSentinelConnection ? config.SentinelUser : config.User;
// Note that we need "" (not null) for password in the case of 'nopass' logins
var password = (isSentinelConnection ? config.SentinelPassword : config.Password) ?? "";
var clientName = Multiplexer.ClientName;

string clientName = Multiplexer.ClientName;
if (!string.IsNullOrWhiteSpace(clientName))
{
clientName = nameSanitizer.Replace(clientName, "");
Expand Down
2 changes: 2 additions & 0 deletions tests/StackExchange.Redis.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ orderby name
"RequestBufferPool",
"ResponseBufferPool",
"responseTimeout",
"sentinelPassword",
"sentinelUser",
"ServiceName",
"SocketManager",
#if !NETFRAMEWORK
Expand Down
47 changes: 47 additions & 0 deletions tests/StackExchange.Redis.Tests/SentinelConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using Xunit;

namespace StackExchange.Redis.Tests;

public class SentinelConfigTests
{
[Fact]
public void Parse_SentinelCredentials_FromConnectionString()
{
var cs = "localhost:26379,serviceName=myprimary,sentinelUser=su,sentinelPassword=sp";
var options = ConfigurationOptions.Parse(cs);

Assert.Equal("su", options.SentinelUser);
Assert.Equal("sp", options.SentinelPassword);
Assert.Equal("myprimary", options.ServiceName);
}

[Fact]
public void ToString_Masks_SentinelPassword_WhenExcluded()
{
var options = new ConfigurationOptions();
options.EndPoints.Add("localhost", 26379);
options.ServiceName = "myprimary";
options.SentinelUser = "su";
options.SentinelPassword = "secret";

var repr = options.ToString(includePassword: false);

Assert.Contains("sentinelUser=su", repr);
Assert.Contains("sentinelPassword=*****", repr);
Assert.DoesNotContain("secret", repr);
}

[Fact]
public void Clone_Preserves_SentinelCredentials()
{
var options = new ConfigurationOptions();
options.SentinelUser = "su";
options.SentinelPassword = "sp";

var clone = options.Clone();

Assert.Equal(options.SentinelUser, clone.SentinelUser);
Assert.Equal(options.SentinelPassword, clone.SentinelPassword);
}
}