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.
Enable
Section titled “Enable”./bin/tellstone --rbac-config policy.yamlACL LOG
Section titled “ACL LOG”ACL LOGReturns the recent rejected-AUTH attempts in chronological order, oldest
first. Each entry is a [timestamp, username, remote address, reason] tuple:
> ACL LOG1) 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. usernamemay be empty when the frame carried no parseable username.reasonis one ofinvalid password,unknown user, ormalformed 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
Section titled “ACL SETUSER”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'+OKACL DELUSER
Section titled “ACL DELUSER”ACL DELUSER <username>Alias of ROLE DELUSER. Removes a user.
Reply: +OK
ACL LIST
Section titled “ACL LIST”ACL LISTAlias 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.
Native binary protocol
Section titled “Native binary protocol”The client package exposes the same operations as typed Go calls:
| Call | Equivalent |
|---|---|
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.
Example
Section titled “Example”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).