3

For elasticache cluster configured to use Redis version 7 or above, there is an option to connect using IAM authentication. With IAM Authentication you can authenticate a connection to ElastiCache for Redis using AWS IAM identities.

Reference: https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/auth-iam.html

I am trying to get an example of working code with StackExchange.Redis client to authenticate requests to elasticache using IAM user

As per the documentation, I have tried the following :

  • Create a role with given policies
  • Created a user with IAM authentication mode
  • Created signed token request and tried to pass it as a password while connecting with cluster

I am getting wrongpass:invalid-username-password error.

1
  • 1
    How did you pass the token? Did you also pass a username, or only password? Can you paste the code you tried already? Commented Dec 7, 2022 at 12:31

1 Answer 1

0

Assuming you have setup your infrastructure correctly (elasticache user and group, security group, VPC/subnet configuration, and the role policy to allow your application to connect to the cache), your application needs to generate SigV4 authentication tokens and use it as the authentication password.

Using the RDSAuthTokenGenerator class from the aws-sdk-net project and the Java samples in the AWS documentation for IAM authentication, I came up with this helper:

public static class ElastiCacheAuthTokenGenerator
{
    private const string SERVICE_NAME = "elasticache";
    private const string REQUEST_METHOD = "GET";
    private const string REQUEST_PROTOCOL = "http";
    private const string URI_SCHEME_DELIMITER = "://";
    private const string PARAM_ACTION = "Action";
    private const string PARAM_USER = "User";
    private const string PARAM_RESOURCE_TYPE = "ResourceType";
    private const string ACTION_NAME = "connect";
    private const string RESOURCE_TYPE_SERVERLESS_CACHE = "ServerlessCache";
    private const string X_AMZ_EXPIRES = "X-Amz-Expires";
    private const string X_AMZ_SECURITY_TOKEN = "X-Amz-Security-Token";
    private static readonly TimeSpan FIFTEEN_MINUTES = TimeSpan.FromMinutes(15);

    public static string GenerateAuthToken(
        AWSCredentials credentials, RegionEndpoint region,
        string userId, string cacheName, bool isServerless
    )
    {
        ImmutableCredentials immutableCredentials = credentials.GetCredentials();
        return GenerateAuthToken(
            immutableCredentials, region,
            userId, cacheName, isServerless
        );
    }

    private static string GenerateAuthToken(
        ImmutableCredentials immutableCredentials, RegionEndpoint region,
        string userId, string cacheName, bool isServerless
    )
    {
        DefaultRequest request = new(
            request: new GenerateElastiCacheAuthTokenRequest(),
            SERVICE_NAME
        )
        {
            HttpMethod = REQUEST_METHOD,
            Endpoint = new UriBuilder(REQUEST_PROTOCOL, cacheName).Uri,
            UseQueryString = true,
        };
        request.Parameters.Add(PARAM_ACTION, ACTION_NAME);
        request.Parameters.Add(PARAM_USER, userId);
        if (isServerless)
        {
            request.Parameters.Add(PARAM_RESOURCE_TYPE, RESOURCE_TYPE_SERVERLESS_CACHE);
        }
        request.Parameters.Add(
            X_AMZ_EXPIRES,
            FIFTEEN_MINUTES.TotalSeconds.ToString(CultureInfo.InvariantCulture)
        );
        if (immutableCredentials.UseToken)
        {
            request.Parameters.Add(X_AMZ_SECURITY_TOKEN, immutableCredentials.Token);
        }

        AWS4SigningResult signingResult = AWS4PreSignedUrlSigner.SignRequest(
            request,
            clientConfig: null,
            metrics: new RequestMetrics(),
            immutableCredentials.AccessKey,
            immutableCredentials.SecretKey,
            service: SERVICE_NAME,
            overrideSigningRegion: region.SystemName
        );

        string authorization = "&" + signingResult.ForQueryParameters;
        Uri url = AmazonServiceClient.ComposeUrl(request);

        // remove the https:// and append the authorization
        return string.Concat(
            url.AbsoluteUri.AsSpan(REQUEST_PROTOCOL.Length + URI_SCHEME_DELIMITER.Length),
            authorization
        );
    }

    private class GenerateElastiCacheAuthTokenRequest : AmazonWebServiceRequest
    {
        public GenerateElastiCacheAuthTokenRequest()
        {
            ((IAmazonWebServiceRequest)this).SignatureVersion = SignatureVersion.SigV4;
        }
    }
}

It allows you to generate a SigV4 token for IAM authentication with ElastiCache, and is only valid for 15 minutes. You can use it directly if your application connects to the cache only once and does not need to handle reauthentication:

string sigV4Token = ElastiCacheAuthTokenGenerator.GenerateAuthToken(
    awsCredentials, awsRegion, userId, cacheName, isServerless
);
ConfigurationOptions options = new()
{
    EndPoints = { { hostname, 6379 } },
    Ssl = true,
    User = userId,
    Password = sigV4Token,
};
ConnectionMultiplexer conn = ConnectionMultiplexer.Connect(options);

If your application requires reauthentication support (most production applications will), you need to use StackExchange.Redis' DefaultOptionsProvider mechanism to ensure the token is not expired when reconnecting. I came up with this very simple implementation which generates a new token each time the password is accessed, but it could be further optimized to reuse a previously generated token if it's still valid for a reasonable period of time (not sure if the performance benefit is worth it though):

public class CustomDefaultOptionsProvider : DefaultOptionsProvider
{
    private readonly AWSCredentials _awsCredentials;
    private readonly RegionEndpoint _awsRegion;
    private readonly string _userId;
    private readonly string _cacheName;
    private readonly bool _isServerless;

    public CustomDefaultOptionsProvider(
        AWSCredentials awsCredentials, RegionEndpoint awsRegion,
        string userId, string cacheName, bool isServerless
    )
    {
        this._awsCredentials = awsCredentials;
        this._awsRegion = awsRegion;
        this._userId = userId;
        this._cacheName = cacheName;
        this._isServerless = isServerless;
    }

    public override string Password => ElastiCacheAuthTokenGenerator.GenerateAuthToken(
        this._awsCredentials, this._awsRegion,
        this._userId, this._cacheName, this._isServerless
    );
}

Now your application code should look like this:

ConfigurationOptions options = new()
{
    EndPoints = { { hostname, 6379 } },
    Ssl = true,
    User = userId,
    Password = null, // set password to null to use Defaults
    Defaults = new CustomDefaultOptionsProvider(
        awsCredentials, awsRegion, userId, cacheName, isServerless
    ),
};
ConnectionMultiplexer conn = ConnectionMultiplexer.Connect(options);

Keep in mind this is not production-tested, there might be a few caveats with this code.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.