How Redis Enterprise’s role based access control (RBAC) works and can be used to improve the security and industrialize Redis ACL for authentication and authorizations on data and commands at scale ?

You can find links to the related video recordings and printable materials at the end of this post.

Explanation and demo video

Prepare the environment

You can use official Redis Enterprise docker containers from docker hub to reproduce the demonstration or practice.

# Start Redis Enterprise
docker run -d --cap-add sys_resource --name redisenterprise -p 8443:8443 -p 9443:9443 -p 12000:12000 -p 12001:12001 -p 12002:12002 redislabs/redis
sleep 20

# Initialize the Cluster
docker exec -d --privileged redisenterprise "/opt/redislabs/bin/rladmin" cluster create name cluster.local username password password
sleep 15


This contents assume that you already know what are Redis ACL. If you did you watch it previously, I recommend that you watch the How to manage security, data-access and permissions with Redis ACL contents before this one. If you want to reproduce the following steps by yourself, you need to install docker, curl and jq. The commands are not purely POSIX shell compliant and need some bash extensions.


A security and compliance issue

Redis is used in nearly every company in the world. The data access control is critical to ensure the data security and to respect the compliance rules such as GDPR. Each incoming connection needs to be authenticated and potentially granted permissions on available databases, commands and data.

Lets imagine that you have three databases :

  • an orders database to store orders and order lines ;
  • a products database to store the product catalog and the product availability in stocks ;
  • a messaging database used as a message bus to communicate between all microservices with Redis PubSub.

Two microservices, each with two endpoints :

  • One microservice to manage orders in the orders database, with : the orders-update endpoint to create and update orders (read-write), and the orders-invoice endpoint to generate invoices (read-only), they also need a read-write and read-only access to the messaging database.
  • The other microservice to manage the product catalog with two endpoints, products-update to update products and products-stocks to check a product availability. They also need a read-write and a read-only access to the messaging database.

There are also people :

  • one administrator, Angélina who should not access the data
  • two project managers, Paul for the orders management, and Pierre for the product catalog. They need to access the logs, the database configuration, the monitoring and to change their projects data. They also need a read-write access to the message bus, the messaging database.
  • one developer David, working on the orders project microservice only, and one developer Denis, working on the products project microservice only. Each of them need a limited access to the monitoring, a read-only access to their own database only and a subscribe-only (read-only) access to the messages in the messaging database.

This is a fairly simple example compared to real enterprise needs. The permissions are easy to implement, but not to scale if you have several projects, several profiles such as developers, managers, architects, administrators, and several databases. Security with ACL administration quickly becomes a nightmare and needs to be industrialized and automated. Lets see how Redis Enterprise manage this example.

Simple Redis ACLs limits

Despite Redis is widely known and used by developers as a database, the security belongs to Ops who are not always aware of the security features. Since version 6, Redis includes authentication by username and data access authorizations with Access Control Lists (ACL).

Ok, lets have a look at their needed permissions, on the cluster and databases. We will potentially need to create the users in each database.

Account Cluster Orders DB Products DB Messaging DB
orders-update None -@all +@hash +@list -@dangerous ~* None -@all +@publish +@subscribe -@dangerous ~*
orders-invoice None -@all +@hash +@list -@dangerous -@write ~* None -@all +@subscribe -@dangerous ~*
products-update None None -@all +@hash +@list -@dangerous ~* -@all +@publish +@subscribe -@dangerous ~*
products-stocks None None -@all +@hash +@list -@dangerous -@write ~* -@all +@subscribe -@dangerous ~*
Angélina Full -@all +@admin +@dangerous -@all +@admin +@dangerous -@all +@admin +@dangerous
Paul Database -@all +@hash +@list -@dangerous ~* None -@all +@publish +@subscribe -@dangerous ~*
Pierre Database None -@all +@hash +@list -@dangerous ~* -@all +@publish +@subscribe -@dangerous ~*
David Monitor -@all +@hash +@list -@dangerous -@write ~* None -@all +@subscribe -@dangerous ~*
Denis Monitor None -@all +@hash +@list -@dangerous -@write ~* -@all +@subscribe -@dangerous ~*

Slides - 11.png

You would have to connect to each database, create all the accounts with a password, then to create the ACLs individually. If a project has any change, it needs to change the ACL definitions for all the relevant users in each database, individually. What a pain to maintain when a new developer join or leave one or several projects, when new projects are created, when projects need access to different datastructures … Let’s try to factorize these.

Implementing roles with Redis Enterprise

On top of ACLs, Redis Enterprise implemented TLS mutual authentication and Roles Based Access Control (RBAC) to make the ACL management more efficient and more scalable with multiple databases. What are the best practices on how to implement authentication, ACLs and roles in Redis Enterprise, and what can be easily achieved with them ?

Redis Enterprise Roles can be compared to groups or profiles. A role grants permissions globally on the Redis enterprise cluster administration and apply specific ACLs on each database. It automatically creates the accounts, configure the ACL and maintain everything up-to-date in each relevant database.

Create databases

You can create them with the web admin interface


or with the REST API

function DBCreate {
  curl -k -u "" --request POST --url "https://localhost:9443/v1/bdbs" --header 'content-type: application/json' --data '{
function DBFindUidByName {
  # Find the DB UID by name
  dbid=`curl -s -k -u "" --request GET --url "https://localhost:9443/v1/bdbs" --header 'content-type: application/json' | jq '.[]|select(.name=="'${dbname}'").uid'`
  echo $dbid

function DBUpdate {
  dbid=`DBFindUidByName "${dbname}"`
  # Update the DB configuration
  curl -k -u "" --request PUT --url "https://localhost:9443/v1/bdbs/${dbid}" --header 'content-type: application/json' --data '{

function DBDelete {
  dbid=`DBFindUidByName "${dbname}"`
  # Delete the database
  curl -k -u "" --request DELETE --url "https://localhost:9443/v1/bdbs/${dbid}" --header 'content-type: application/json'

DBCreate orders 102400 '"port":12000'
DBCreate products 102400 '"port":12001'
DBCreate messaging 102400 '"port":12002'


The final result is the same and all the databases are successfully created :


Create the accounts

Then, we need to create all the accounts for the endpoints and for the users. We create them in the cluster and the cluster will automatically maintain a copy of them in the relevant databases.

Slides - 21.png

The accounts can be created with the web interface


Or with the REST API, to industrialize the creation and automate it.

function UserCreate {
  curl -k -u "" --request POST --url "https://localhost:9443/v1/users" --header 'content-type: application/json' --data '{
     "role": "none"

function UserFindUidByName {
  # Find Account UID by name
  uuid=`curl -s -k -u "" --request GET --url "https://localhost:9443/v1/users" --header 'content-type: application/json' | jq '.[]|select(.name=="'${username}'").uid'`
  echo ${uuid}

function UserUpdate {
  uuid=`UserFindUidByName "${username}"`
  curl -k -u "" --request PUT --url "https://localhost:9443/v1/users/${uuid}" --header 'content-type: application/json' --data '{'"${usercfg}"'}'

function UserDelete {
  uuid=`UserFindUidByName "${username}"`
  curl -k -u "" --request DELETE --url "https://localhost:9443/v1/users/${uuid}" --header 'content-type: application/json'

# Users
for u in orders-update orders-invoice products-update products-stocks Angelina Paul Pierre David Denis; do
  UserCreate "${u}" "${u}" "${u}"


At the end, the result is the same and all the accounts are created in the cluster :


Create the ACL

We have to define these ACLs in the cluster. They will be some sort of ACL templates to apply tu users in specific databases, depending on the granted roles. So, we need to find all the possible ACL to apply and define them in the cluster. We can easily identify five distinct ACL : read-write access or read-only access to data, publish-subscribe or subscribe-only access to channels, and database administration.

ACL Name ACL definition
DataRW -@all +@hash +@list -@dangerous ~*
DataRO -@all +@hash +@list -@dangerous -@write ~*
MsgRW -@all +@publish +@subscribe -@dangerous ~*
MsgRO -@all +@subscribe -@dangerous ~*
DBAdmin -@all +@admin +@dangerous

Slides - 27.png

The ACLs can be created with the web administration interface


or with REST API calls. I wrote a bash function helper to make them more friendly.

function ACLCreate {
  curl -k -u "" --request POST --url "https://localhost:9443/v1/redis_acls" --header 'content-type: application/json' --data '{"name": "'"${aclname}"'", "acl": "'"${acltext}"'" }' 

function ACLFindUidByName {
  # Find ACL UID by name
  uuid=`curl -s -k -u "" --request GET --url "https://localhost:9443/v1/redis_acls" --header 'content-type: application/json' | jq '.[]|select(.name=="'"${aclname}"'").uid'`
  echo ${uuid}

function ACLUpdate {
  acluid=`ACLFindUidByName "${aclname}"`
  curl -k -u "" --request PUT --url "https://localhost:9443/v1/redis_acls/${acluid}" --header 'content-type: application/json' --data '{"acl":"'"${acltext}"'"}'

function ACLDelete {
  acluid=`ACLFindUidByName "${aclname}"`
  curl -k -u "" --request DELETE --url "https://localhost:9443/v1/redis_acls/${acluid}" --header 'content-type: application/json'

ACLCreate "DataRW"  '-@all +@hash +@list -@dangerous ~*'
ACLCreate "DataRO"  '-@all +@hash +@list -@dangerous -@write ~*'
ACLCreate "MsgRW"   '-@all +publish +subscribe -@dangerous ~*'
ACLCreate "MsgRO"   '-@all +subscribe -@dangerous ~*'
ACLCreate "DBAdmin" '-@all +@admin +@dangerous'


Create the roles

With these definitions, we can more easily see patterns :

Account Cluster Orders DB Products DB Messaging DB
orders-update None DataRW None MsgRW
orders-invoice None DataRO None MsgRO
products-update None None DataRW MsgRW
products-stocks None None DataRO MsgRO
Angélina Full DBAdmin DBAdmin DBAdmin
Paul Database DataRW None MsgRW
Pierre Database None DataRW MsgRW
David Monitor DataRO None MsgRO
Denis Monitor None DataRO MsgRO

The Redis Enterprise cluster can grant cluster permissions to roles and each Redis Enterprise database can grant specific ACLs to a role. The idea is to create a distinct role per unique permission set, each unique line in the table. Thus, for our simple case, we need to create nine roles, one for each possible combination. A role is an account profile or an account group with the same permissions on the cluster and on the databases.

Roles Cluster admin. Orders DB Products DB Messaging DB
Orders-RW None DataRW None MsgRW
Orders-RO None DataRO None MsgRO
Products-RW None None DataRW MsgRW
Products-RO None None DataRO MsgRO
Global-ADM Admin DBAdmin DBAdmin DBAdmin
Orders-PM DB-Member DataRW None MsgRW
Products-PM DB-Member None DataRW MsgRW
Orders-DEV DB Viewer DataRO None MsgRO
Products-DEV DB-Viewer None DataRO MsgRO

Slides - 37.png

The cluster permissions are directly and globally assigned to the role, let’s create them with the web administration interface first


And then with the REST API and an helper function

function RoleCreate {
  curl -k -u "" --request POST --url "https://localhost:9443/v1/roles" --header 'content-type: application/json' --data '{"name": "'"${rolename}"'", "management": "'"${roleperm}"'" }'  

function RoleFindUidByName {
  # Find Role UID by name
  ruid=`curl -s -k -u "" --request GET --url "https://localhost:9443/v1/roles" --header 'content-type: application/json' | jq '.[]|select(.name=="'"${rolename}"'").uid'`
  echo ${ruid}

function RoleUpdate {
  roleuid=`RoleFindUidByName "${rolename}"`
  curl -k -u "" --request PUT --url "https://localhost:9443/v1/roles/${roleuid}" --header 'content-type: application/json' --data '{"management":"'"${roleperm}"'"}'

function RoleDelete {
  roleuid=`RoleFindUidByName "${rolename}"`
  curl -k -u "" --request DELETE --url "https://localhost:9443/v1/roles/${roleuid}" --header 'content-type: application/json'

RoleCreate "Orders-RW"    "none"
RoleCreate "Orders-RO"    "none"
RoleCreate "Products-RW"  "none"
RoleCreate "Products-RO"  "none"
RoleCreate "Global-ADM"   "admin"
RoleCreate "Orders-PM"    "db_member"
RoleCreate "Products-PM"  "db_member"
RoleCreate "Orders-DEV"   "db_viewer"
RoleCreate "Products-DEV" "db_viewer"


Map ACLs to roles in each database

Then, in each database, we have to define if a specific ACL template should be applied to the members of specific roles. Let’s start with the orders database.

Slides - 46.png

The Web administration interface makes some convenience abstractions to map ACLs to Roles for each database under the unified roles page.


The REST API is operating at a lower level, directly manipulating the role-acl mapping in the database definitions. Let’s create the mapping for the orders database.

Roles Orders DB
Orders-RW DataRW
Orders-RO DataRO
Global-ADM DBAdmin
Orders-PM DataRW
Orders-DEV DataRO

I define a first bash helper function, to lookup roles and ACLs uid and build the mapping JSON objects, and a second one to apply a configuration change to a database.

function RoleACLMap {
  roleuid=`RoleFindUidByName "${rolename}"`
  acluid=`ACLFindUidByName "${aclname}"`
  # Build the JSON object to map the ACL to the role
  echo '{"redis_acl_uid": '${acluid}',"role_uid": '${roleuid}'}'

mapping="`RoleACLMap Orders-RW DataRW`"
mapping="${mapping},`RoleACLMap Orders-RO DataRO`"
mapping="${mapping},`RoleACLMap Global-ADM DBAdmin`"
mapping="${mapping},`RoleACLMap Orders-PM DataRW`"
mapping="${mapping},`RoleACLMap Orders-DEV DataRO`"
DBUpdate "orders" '"roles_permissions":['"$mapping"']'

We can check the result in the web interface.


Let’s create the mapping for the products database.

Roles Products DB
Products-RW DataRW
Products-RO DataRO
Global-ADM DBAdmin
Products-PM DataRW
Products-DEV DataRO
mapping="`RoleACLMap Products-RW DataRW`"
mapping="${mapping},`RoleACLMap Products-RO DataRO`"
mapping="${mapping},`RoleACLMap Global-ADM DBAdmin`"
mapping="${mapping},`RoleACLMap Products-PM DataRW`"
mapping="${mapping},`RoleACLMap Products-DEV DataRO`"
DBUpdate "products" '"roles_permissions":['"$mapping"']'


And finally, define the mappings for the messaging database.

Roles Messaging DB
Orders-RW MsgRW
Orders-RO MsgRO
Products-RW MsgRW
Products-RO MsgRO
Global-ADM DBAdmin
Orders-PM MsgRW
Products-PM MsgRW
Orders-DEV MsgRO
Products-DEV MsgRO
mapping="`RoleACLMap Orders-RW MsgRW`"
mapping="${mapping},`RoleACLMap Orders-RO MsgRO`"
mapping="${mapping},`RoleACLMap Products-RW MsgRW`"
mapping="${mapping},`RoleACLMap Products-RO MsgRO`"
mapping="${mapping},`RoleACLMap Global-ADM DBAdmin`"
mapping="${mapping},`RoleACLMap Orders-PM MsgRW`"
mapping="${mapping},`RoleACLMap Products-PM MsgRW`"
mapping="${mapping},`RoleACLMap Orders-DEV MsgRO`"
mapping="${mapping},`RoleACLMap Products-DEV MsgRO`"
DBUpdate "messaging" '"roles_permissions":['"$mapping"']'


Disable default anonymous account

By default, if an authentication idid not occur or fails, because the password is wrong or because the account does not exist in the database, the connexion is assigned to the default anonymous user, with default allcommands on allkeys permissions. We do not want that. we defined restricted permissions, we need to disable the default anonymous account in each database.

Either from the web administration interface :


Or with the REST API.

DBUpdate "orders" '"default_user": false'
DBUpdate "products" '"default_user": false'
DBUpdate "messaging" '"default_user": false'

Assign a roles to each account

So, for each project, we defined

  • a read-only application role,
  • a read-write application role,
  • a read-ony with limited admin permission developer role,
  • a read-write product manager role on the needed database for each microservice and a global administrator only role.

Lets assign roles to the accounts :

Account Role
orders-update Orders-RW
orders-invoice Orders-RO
products-update Products-RW
products-stocks Products-RO
Angélina Global-ADM
Paul Orders-PM
Pierre Products-PM
David Orders-DEV
Denis Products-DEV

Slides - 55.png

Once again, either from the web administration interface :


Or with the REST API :

function UserSetRole {
  roleuid=`RoleFindUidByName "${rolename}"`
  UserUpdate "${username}" '"role_uids":['${roleuid}']'
UserSetRole "orders-update" "Orders-RW"
UserSetRole "orders-update" "Orders-RW"
UserSetRole "orders-invoice" "Orders-RO"
UserSetRole "products-update" "Products-RW"
UserSetRole "products-stocks" "Products-RO"
UserSetRole "Angelina" "Global-ADM"
UserSetRole "Paul" "Orders-PM"
UserSetRole "Pierre" "Products-PM"
UserSetRole "David" "Orders-DEV"
UserSetRole "Denis" "Products-DEV"



Permissions tests

I wrote a script to test a set of meaningful commands with each account on each database.

cat << "EOF" >

function CmdTest {
echo -en " - ${DESC} (${P}${CMD}${N}): "
RES=`echo "${CMD}" | timeout 1 redis-cli --user ${DBUSER} --pass ${DBPASS} -p ${DBPORT} 2> /dev/null | tr '\r' ','`
echo "${RES}" | grep "${ERR}" > /dev/null 2>&1
[ $? -eq 0 ] && echo -en ${R} || echo -en ${G}
echo -e ${RES}${N}

function BatchTest {
echo -e ${LG}Database${N}: ${Y}${DBNAME}${N}\(${Y}${DBPORT}${N}\), ${LG}User${N}: ${B}${DBUSER}${N}, ${LG}Password${N}: ${B}${DBPASS}${N}
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "STRING Write            " "SET key value" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "STRING Read             " "GET key" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "HASH Write              " "HSET order:key field1 value1 field2 value2" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "HASH Read               " "HGETALL order:key" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "LIST Write              " "LPUSH order:key:sub value1" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "LIST Write              " "LPUSH order:key:sub value2" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "LIST Read               " "LLEN order:key:sub" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "LIST Read+Write         " "LPOP order:key:sub" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "PUBSUB Write            " "PUBLISH channel msg" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "PUBSUB Read             " "SUBSCRIBE channel" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "STREAM Write            " 'XADD mystream * item 1' '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "STREAM Read             " "XLEN mystream" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'
CmdTest ${DBPORT} ${DBUSER} ${DBPASS} "Generic commands        " "INFO KEYSPACE" '\(ERR\|WRONGPASS\|(error)\|NOAUTH\|NOPERM\)'

for account in orders-update orders-invoice products-update products-stocks Angelina Paul Pierre David Denis Didier; do
BatchTest ${account} ${account} orders 12000
BatchTest ${account} ${account} products 12001
BatchTest ${account} ${account} messaging 12002
chmod +x

For orders-update


For orders-invoice


and so on, you can execute it yourself with the code available in my blog or zoom on my results in the Video.

Permissions maintenance

We initialized the projects, but things can change over the time.

A new employee joins

A new developer, Didier, joins the orders team.

Slides - 56.png

We only have to give him the Orders-DEV role and he will have the appropriate permissions on the cluster and the data.

UserCreate "Didier" "" "Didier"
UserSetRole "Didier" "Orders-DEV"

A developer switches to another team

The existing developer, David, moves to the products team.

Slides - 57.png

We only have to change his role, and he will no longer have his previous permissions, but only the new permissions.

UserSetRole "David" "Products-DEV"

An employee leaves

A developer, Denis, leaves the company.

Slides - 58.png

We only have to delete or disable this accounts.

UserDelete "Denis"

Access to new structures

Streams usage needed in messaging DB.

Slides - 59.png

The messaging features need to use streams commands, from now, we only have to update the Msg-RO and Msg-RW ACLs.

ACLUpdate "MsgRW"   '-@all +publish +subscribe +@stream -@dangerous ~*'
ACLUpdate "MsgRO"   '-@all +subscribe +@stream -@write -@dangerous ~*'

Wrap-up and LDAP

Applications, developers, project managers and admins are uniquely identified with their own authentication and have specific permissions on the cluster and on the data. My next contents will talk about LDAP to roles mapping, because it is a pain to maintain when new people are hired, when people leave, when people move from one project to another, when people work on several projects… whereas all these information are already available and maintained up-to-date in the Enterprise LDAP.

Materials and Links

Link Description
Video Video with explanation and demo