Skip to content

ACL

The ACL command family manages users and exposes the auth-failure audit trail over RESP2. ACL SETUSER, DELUSER, and LIST are aliases for their ROLE counterparts; ACL LOG is unique to this family and reads the server’s recent rejected-AUTH buffer.

The family requires role-based access control to be enabled (--rbac-config) and the caller to hold the ACL permission (granted by the admin category) — otherwise the server replies -ERR RBAC is not enabled or -NOPERM.

Terminal window
./bin/tellstone --rbac-config policy.yaml
ACL LOG

Returns the recent rejected-AUTH attempts in chronological order, oldest first. Each entry is a [timestamp, username, remote address, reason] tuple:

> ACL LOG
1) 1) "2026-08-04T10:11:12Z"
2) "alice"
3) "127.0.0.1:54321"
4) "invalid password"
2) 1) "2026-08-04T10:11:20Z"
2) "mallory"
3) "10.0.0.7:38210"
4) "unknown user"
  • The timestamp is RFC3339 (e.g. 2026-08-04T10:11:12Z) at record time.
  • username may be empty when the frame carried no parseable username.
  • reason is one of invalid password, unknown user, or malformed request (the last only over the binary protocol).

The buffer holds the 100 most recent failures (DefaultAuthLogCap); the oldest entry is evicted once full. Only the failed-AUTH path records — never the data hot path. ACL LOG returns an empty array when nothing has failed.

Unlike Redis’s keyed field map, the reply is a flat array of tuples — the content (who failed, when, from where, why) is the same.

Reply: an array of [timestamp, username, remote address, reason] tuples.

ACL SETUSER <username> <role> [>password] [nopass]

Alias of ROLE SETUSER. Creates or updates a user, binding it to a role that must already exist. At least one password option is required: >password bcrypt-hashes the plaintext server-side, nopass marks a passwordless user. When both are given, the last option wins.

Reply: +OK

> ACL SETUSER bob operator '>bobpw'
+OK
ACL DELUSER <username>

Alias of ROLE DELUSER. Removes a user.

Reply: +OK

ACL LIST

Alias of ROLE GETUSER for every user: one entry per user, sorted by name, with the username, bound role (null when the default role applies), whether a password is set, and the role’s granted commands and namespace whitelist. Password hashes are never rendered.

Reply: an array of 5-element arrays per user.

The client package exposes the same operations as typed Go calls:

CallEquivalent
c.AclSetUser(username, role, passOptions, scratch)ACL SETUSER <user> <role> [>pw] [nopass]passOptions are []byte(">pw") / []byte("nopass")
c.AclDelUser(username, scratch)ACL DELUSER <user>
c.AclList(scratch)ACL LIST — returns []ACLUser
c.AclLog(scratch)ACL LOG — returns []AuthLogEntry{Timestamp, Username, RemoteAddr, Reason}

AuthLogEntry.Timestamp is the same RFC3339 rendering the RESP handler emits, so both protocols expose identical log content.

c, _ := client.Dial("127.0.0.1:9988", 2*time.Second)
defer c.Close()
scratch := make([]byte, 4096)
if err := c.Auth("alice", "alicepw", scratch); err == nil {
log.Fatal("expected AUTH to fail")
}
entries, err := c.AclLog(scratch)
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
fmt.Printf("%s %s %s %s\n", e.Timestamp, e.Username, e.RemoteAddr, e.Reason)
}

The binary wire format for the log is [2B entryCount] then, per entry, four length-prefixed fields: timestamp, username, remote address, reason.

A full walkthrough ships in the main repository at cmd/example/acl: it authenticates as an admin, creates a role and a user via the ACL family, then provokes a failed AUTH and reads the resulting entry back through ACL LOG (or c.AclLog).