Connecting to Redis with an ACL user that cannot run INFO
A Redis ACL user without the INFO grant connects and browses keys, while health, overview and performance raise a permission error and the connection test reports degraded.
Hand a Redis ACL user everything except +info and the session still works. Keys
browse, commands run, the results grid fills. What stops is the monitoring: the
three reads whose only source is INFO come back as the server’s own NOPERM
refusal. That is not a broken connection and it is not a healthy one, so the
connection test has a third answer for it.
The connection fields, and which one is the ACL user
The Redis provider consumes discrete fields, not a connection string:
supportsConnectionString is false. The dialog collects six.
| Field | Required | What it becomes |
|---|---|---|
host |
yes | throws DatabaseConfigError in validate() if missing |
port |
no | defaults to 6379 |
user |
no | the Redis 6 ACL user, sent as ioredis’s username |
password |
no | ioredis’s password; omit for an unauthenticated instance |
database |
no | logical DB index, parsed as int, defaults to 0 |
ssl |
no | mapped onto ioredis’s single tls option |
The Username box is the ACL user and nothing else. When it is empty the provider
passes no username at all, because ioredis authenticates as default only when
the field is absent - a plain requirepass server has no ACL user to name, and
sending an empty one is not the same as sending none.
A pasted URL fills the same box. redis://analytics:pw@host:6379/0 is decomposed
by the UI parser into host, port, user, password and database before the provider
sees anything, and the raw string is dropped. Because it is dropped, the scheme’s
TLS intent has to travel as a field: rediss:// arrives as sslMode: 'require'
and redis:// as 'disable'.
There is a version of this that fails silently, and it is the one this behaviour
replaced. connectionFields decides what a save writes, and it omitted user
for redis until 2026-08-27, so the name a person typed was discarded between
the box and the driver. Sessions ran as default, and health went green under
someone else’s permissions. Measured against redis:latest on 2026-08-26, with
default left on nopass ~* &* +@all and a second user defined
on >probepw ~* +@all -info:
{host, port, password} -> ACL WHOAMI = default | INFO succeeds
{host, port, username, password} -> ACL WHOAMI = probe | INFO refused: NOPERM
Both arms are the measurement. One arm alone would only have shown that a request succeeded.
What a user without INFO can still do
Everything the key browser is made of, because none of it reads INFO.
Schema discovery is a cursor-based SCAN with COUNT 100, never KEYS *, and
it stops at 1000 keys. Each key is grouped by everything before its first colon
plus :*, so user:123 and user:456 collapse into a user:* row, and each
prefix is probed with TYPE until three distinct value types have been observed.
The resulting list is sorted by descending key count.
The editor is the other half, and it is equally indifferent to INFO. A query is
dispatched through ioredis’s generic client.call(), in one of two formats
decided by the first character of the first runnable line: a leading { is a JSON
command object, anything else a plain command through a quote-aware tokenizer.
Any command the ACL allows works, with no per-command code in the provider.
The connection itself survives by design rather than by accident. ioredis’s own
ready check calls INFO; on NOPERM it logs Skipping the ready check and
connects anyway.
Two monitoring surfaces survive as well, and that was deliberate. The
SLOWLOG GET 10 and CLIENT LIST reads behind the Queries and Sessions panels
are wrapped in try/catch and degrade to [] rather than throwing, so an ACL that
forbids those two commands does not take the dashboard down with it.
Why the health reads raise Redis’s own NOPERM for the INFO command
Three methods have exactly one source. getHealth() reads INFO.
getOverview() reads INFO plus DBSIZE. getPerformanceMetrics() reads
INFO. Connected clients, memory in use, uptime, the server version, the cache
hit ratio and instantaneous_ops_per_sec are all parsed out of that one bulk
reply.
Take INFO away from the user and there is nothing left to parse. So the limit,
stated plainly: a user without the INFO grant still connects and browses keys,
but health, overview and performance raise the server’s own permission error
rather than answering with fabricated zeros.
The alternative was available and was refused. Every field those panels want has
an obvious default - clients 0, memory 0 B, hit ratio 100. The cache hit ratio
already carries a fallback of 100.0 for a server that has served no traffic, so
the machinery for a plausible-looking zero exists. Filling the panels that way
would produce a dashboard that reads as a healthy idle server and is in fact a
permission error, and no operator can tell those apart by looking. The NOPERM reply is
the server’s own answer, and it is the only output here that tells you what to
change.
Green, amber and failed are three different results
POST /api/db/test-connection reports a restricted user as degraded, amber:
not a green tick and not a failure. The connection is real. The health read is
not.
Two outcomes would have to lie in one direction or the other. Green would mean “connected and readable” for a session whose entire monitoring surface is a permission error, which is the fabricated-zeros dashboard with a nicer icon. Failed would mean “do not use this connection” about a user who can browse the whole keyspace and run every command the ACL grants - which sends someone to debug a network path that works.
Amber says the thing that is actually true, and it is the useful outcome because
it is the only one that carries a next step. The next step is a decision, not a
fix: grant +info to this user, or accept that the monitoring panels stay empty
for it and use the key browser and the editor, which are unaffected.
Choosing the grants a browsing user needs
Work backwards from the commands the app sends. Every surface here names its own.
| Surface | Commands it sends |
|---|---|
| Key pattern tree | SCAN, TYPE |
| Scan Keys and value reads | SCAN, GET, HGETALL, LRANGE, SMEMBERS, ZRANGE |
| Health, overview, performance | INFO, DBSIZE |
| Queries panel | SLOWLOG GET |
| Sessions panel | CLIENT LIST |
| Storage panel | INFO memory |
| Operations, Server Info | INFO |
A user granted the first two rows browses and reads. Adding +info turns the
amber to green. The Queries and Sessions panels are the two that are safe to
withhold, because their absence is already handled as an empty list.
The reason to bother getting this right is that the ACL is the enforcement point,
not the provider. The generic call() dispatch runs SET, DEL and FLUSHALL
the same way it runs GET; there is no read-only guard in the Redis provider, so
access control is whatever the ACL enforces. The security page makes
the same point about the controls Studio does apply: masking is display-level,
and a hard guarantee needs database-side grants on the account the connection
uses. A read-only Redis session is an ACL you wrote, or it is nothing.
To reproduce the whole thing locally, including the amber:
docker run --rm -d --name redis-acl -p 6389:6379 redis:latest
docker exec redis-acl redis-cli ACL SETUSER probe on '>probepw' '~*' +@all -info
Connect on port 6389 with username probe and password probepw: keys browse,
and the connection test reports degraded. Leave the Username box empty and the
session authenticates as default, whose INFO succeeds and whose test comes
back green.