ElastiCache RBAC key-prefix ACLs give you per-tenant Redis isolation on one cluster
Multi-tenant SaaS. Every tenant gets a cache namespace. The naive approach is a Redis cluster per tenant — isolated, but $6/mo/tenant before you’ve stored a single key.
ElastiCache RBAC with key-prefix ACLs solves this. One cluster, one user per tenant, each user locked to their own key prefix.
The ACL pattern
on ~{tenant_name}-* +@read +@write -@dangerous
on— user is active~{tenant_name}-*— can only touch keys starting with this prefix+@read +@write— full read/write access within that prefix-@dangerous— no FLUSHDB, FLUSHALL, DEBUG, etc.
Three tiers in practice:
| User | ACL | Purpose |
|---|---|---|
auth-handler | ~auth-handler-* +@read +@write -@dangerous | Shared service, own prefix |
tenant-50 | ~tenant-50-* +@read +@write -@dangerous | Per-tenant, created by operator |
redis-superadmin | ~* +@all | Admin access, break-glass |
Terraform
resource "aws_elasticache_user" "auth_handler" {
user_id = "${var.identifier}-auth-handler"
user_name = "auth-handler"
access_string = "on ~auth-handler-* +@read +@write -@dangerous"
engine = "REDIS"
authentication_mode {
type = "password"
passwords = [random_password.auth_handler_user.result]
}
}
Per-tenant users are created dynamically by a Kopf operator when a Tenant CR is applied:
async def create_tenant_redis_user(tenant_name: str) -> tuple[str, str]:
password = generate_password()
access_string = f"on ~{tenant_name}-* +@read +@write -@dangerous"
client.create_user(
UserId=user_id,
UserName=tenant_name,
Engine="REDIS",
AccessString=access_string,
AuthenticationMode={"Type": "password", "Passwords": [password]},
)
return tenant_name, password
The operator also adds the new user to the ElastiCache user group so it takes effect immediately.
Verifying isolation
$ redis-cli --tls -h cluster.cache.amazonaws.com --user tenant-50 --pass $PASS
# Own prefix: works
> SET tenant-50-session abc
OK
> GET tenant-50-session
"abc"
# Foreign prefix: denied
> GET auth-handler-session
(error) NOPERM this user has no permissions to access one of the keys
> SET auth-handler-probe "should-fail"
(error) NOPERM this user has no permissions to access one of the keys
No key enumeration across prefixes either — SCAN only returns keys matching the ACL pattern.
The Terraform gotcha
The operator creates users and adds them to the user group at runtime. Terraform doesn’t know about these users. On the next terraform plan, it sees users it didn’t create and tries to remove them:
# aws_elasticache_user_group.app will be updated in-place
~ user_ids = [
- "tenant-50",
# (removed by Terraform)
]
Fix:
resource "aws_elasticache_user_group" "app" {
user_group_id = "${var.identifier}-users"
user_ids = [
aws_elasticache_user.default.user_id,
aws_elasticache_user.app.user_id,
aws_elasticache_user.auth_handler.user_id,
aws_elasticache_user.superadmin.user_id,
]
lifecycle {
ignore_changes = [user_ids]
}
}
ignore_changes = [user_ids] tells Terraform to stop fighting the operator. Terraform manages the base users. The operator manages tenant users. They coexist.
Cost
One cache.t4g.micro ($6/mo). Unlimited tenants. Each tenant gets read/write isolation at the Redis layer — no application-level trust required.
The alternative — a cluster per tenant — starts at $6/mo/tenant and scales linearly. At 100 tenants that’s $600/mo in Redis alone. With RBAC prefix ACLs it’s still $6/mo.