Using IAM authentication for Redis on AWS
Tags: redis, go, security, tutorial
Published: 2023-12-07 08:27:34

*How to securely connect your Go applications to Amazon MemoryDB (or ElastiCache) for Redis using IAM*

Amazon MemoryDB for Redis has supported username/password based authentication [using Access Control Lists](https://docs.aws.amazon.com/memorydb/latest/devguide/components.html#whatis.components.acls) since the very beginning. But you can also use [IAM  based authentication](https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-memorydb-redis-iam-authentication/) that allows you to associate IAM users and roles with MemoryDB users so that applications can use IAM credentials to authenticate to the MemoryDB cluster. With this authentication method, you don't need to use a (long-lived) user password. Instead, you use an authentication token generated using [AWS Signature Version 4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html).

There are many benefits to this approach. Instead of managing username and password based credentials, you can use IAM to centrally manage access to MemoryDB clusters. For client applications running on Amazon EC2, Amazon EKS, AWS Lambda, AWS App Runner etc., you can inject these credentials (depending on the platform e.g. profile credentials in EC2 and [instance role in App Runner](https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles)) - this provides greater security.

[MemoryDB documentation](https://docs.aws.amazon.com/memorydb/latest/devguide/auth-iam.html#auth-iam-Connecting) has an example for a Java application with the [Lettuce client](https://lettuce.io/). The process is similar for other languages, but you still need to implement it. So, let's learn how to do it for a Go application with the widely used [go-redis](https://github.com/redis/go-redis) client.

> As a bonus, this is also applicable to [ElastiCache for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/auth-iam.html), which also supports IAM authentication. There are minor differences which I will list at the end of the blog.

Let's get started....

## 1. Configure the MemoryDB cluster along with user and IAM role

Start by creating a [MemoryDB user, Access Control list (ACL) and add the user](https://docs.aws.amazon.com/memorydb/latest/devguide/clusters.acls.html#users-management) to it. Make sure to use **IAM** as the authentication type.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6veeyyde3cidbbdlggj.jpg)

Now, create the MemoryDB cluster. Make sure to choose version **7.0 or above** and choose the ACL you created before.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r994t2jivstof4zcc6k.jpg)

While the cluster is being provisioned, [create an IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html#roles-creatingrole-service-console). Use the following **Trust Relationship** to allow connectivity from an EC2 instance:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Principal": {
                "Service": [
                    "ec2.amazonaws.com"
                ]
            }
        }
    ]
}
```

Use the following permissions - replace the MemoryDB cluster, IAM username, AWS account ID and region:

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                memorydb:Connect"
            ],
            "Resource": [
                "arn:aws:memorydb:<enter aws region>:<enter aws account ID>:cluster/<enter memorydb cluster name>",
                "arn:aws:memorydb:<enter aws region>:<enter aws account ID>:user/<enter memorydb iam user name>"
            ]
        }
    ]
}
```

## 2. Configure AWS Cloud9 environment

Create an AWS Cloud9 environment to execute the client application that will connect to MemoryDB. 

- Make sure to use same VPC and subnet as the MemoryDB cluster
- Update the MemoryDB subnet group to add inbound rule for the client application connectivity.

Refer to the [Getting started with Redis on AWS - the easy way!](https://dev.to/aws/getting-started-with-redis-on-aws-the-easy-way-jol) blog post that already has this covered.

## 3. Run the client app

Once the MemoryDB cluster and Cloud9 instance are ready, go ahead and execute the client application. 

Open the Cloud9 instance terminal, clone the following GitHub repository:

```
git clone https://github.com/build-on-aws/aws-redis-iam-auth-golang
cd aws-redis-iam-auth-golang
```

Update the `run.sh` file to enter the MemoryDB cluster endpoint, and username. For example:

```bash
export SERVICE_NAME=memorydb
export CLUSTER_NAME=demo-iam-cluster
export CLUSTER_ENDPOINT=clustercfg.demo-iam-cluster.xyzxy4.memorydb.us-west-1.amazonaws.com:6379
export USERNAME=demo-iam-user
```

Once the app is up and running, simply invoke it's HTTP endpoints to verify that it works with IAM authentication.

```bash
curl -i -X POST -d '{"key":"foo1", "value":"bar2"}' localhost:8080
curl -i -X POST -d '{"key":"foo2", "value":"bar2"}' localhost:8080

curl -i localhost:8080/foo1

curl -i localhost:8080/not_there
```

The application super simple. That's on purpose. Let's move on to the important bits, which is about the IAM authentication part.

## IAM authentication logic - Behind the scenes

> You can refer to the authentication code [in the GitHub repository](https://github.com/build-on-aws/aws-redis-iam-auth-golang/blob/main/auth/auth.go)

1) We first create a `HTTP` `GET` request that contains the cluster name as part of the URL along with username, the action (`connect`) and token expiry ([900 seconds](https://docs.aws.amazon.com/memorydb/latest/devguide/auth-iam.html#auth-iam-limits)) as query parameters:

```go
//...
  queryParams := url.Values{
    "Action":        {connectAction},
    "User":          {userName},
    "X-Amz-Expires": {strconv.FormatInt(int64(tokenValiditySeconds), 10)},
  }

  authURL := url.URL{
    Host:     clusterName,
    Scheme:   "http",
    Path:     "/",
    RawQuery: queryParams.Encode(),
  }

  req, err := http.NewRequest(http.MethodGet, authURL.String(), nil)
//...
```

2) The request is then signed using [PresignHTTP](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws/signer/v4#Signer.PresignHTTP):

```go
func (atg AuthTokenGenerator) Generate() (string, error) {

	signedURL, _, err := atg.signer.PresignHTTP(
		context.Background(),
		atg.credentials,
		atg.req,
		hexEncodedSHA256EmptyString,
		atg.serviceName,
		atg.region,
		time.Now().UTC(),
	)

  //...
}
```

3) The token generator is invoked in a `CredentialsProvider` during client creation:

```go
//...
	generator, err := auth.New(serviceName, clusterName, username, region)

	client = redis.NewClusterClient(
		&redis.ClusterOptions{
			Username: username,
			Addrs:    []string{clusterEndpoint},
			NewClient: func(opt *redis.Options) *redis.Client {

				return redis.NewClient(&redis.Options{
					Addr: opt.Addr,

					CredentialsProvider: func() (username string, password string) {
						token, err := generator.Generate()
						return opt.Username, token
					},
					TLSConfig: &tls.Config{InsecureSkipVerify: true},
				})
			},
		})
//...
```

## This can also be used for ElastiCache!

The same approach applies - with a few changes. First, the IAM policy has to be updated to reflect ElastiCache resources (obviously!)

For example:

```
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect" : "Allow",
        "Action" : [
          "elasticache:connect"
        ],
        "Resource" : [
          "arn:aws:elasticache:<enter aws region>:<enter aws account id>:<enter type - replicationgroup or serverlesscache>:<enter replicationgroup or serverlesscache name>",
          "arn:aws:elasticache:<enter aws region>:<enter aws account id>:user:<enter username>"
        ]
      }
    ]
  }
```

Before you run the application, update the `SERVICE_NAME` environment variable to `elasticache` as well as the endpoint URL, cluster name and IAM username for the ElastiCache instance.

This example assumes you are using a Redis Cluster connection mode (which is the only option in case of MemoryDB). But in case of ElastiCache be mindful of whether you are using [cluster-mode enabled configuration](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Replication.Redis-RedisCluster.html). If *not*, you will have to tweak the code to use `redis.NewClient` (instead of `redis.NewClusterClient`). The `CredentialsProvider` option will be available nonetheless.

## Conclusion

Using IAM authentication has its benefits. Instead of managing username and passwords in multiple locations/applications, delegate the heavy lifting to IAM. All you need is to provide configure appropriate permissions (principle of "least privilege").

But you also need to be aware of the limitations. For e.g. IAM authentication is not supported in `MULTI EXEC` commands. For a complete list, [refer to the documentation](https://docs.aws.amazon.com/memorydb/latest/devguide/auth-iam.html#auth-iam-limits).

Have you tried using IAM authentication with MemoryDB or ElastiCache for Redis in other programming languages? Let me know.

Happy building!