Skip to content

Role-Based Access Control (RBAC)

Tellstone supports per-user authentication with role-based access control (RBAC). Where --require-pass authenticates every client against a single shared password, RBAC binds each user to a role that decides which commands the user may run on which keys. RBAC supersedes --require-pass when both are configured.

RBAC is off by default and adds no overhead when it is not enabled.

Point --rbac-config at a policy file (YAML or JSON):

Terminal window
./bin/tellstone --rbac-config policy.yaml --enable-resp

The policy file is validated at startup — unknown roles, malformed rules, and duplicate names fail the load so a bad file can never half-apply. A validated file is re-read on SIGHUP for hot reload (see Hot reload).

policy.yaml
roles:
- name: admin
rules: ["+@all", "~*"]
- name: readonly
rules: ["+@read", "~*"]
users:
- name: admin
role: admin
password: "$2a$10$pcaKkTfRy.KSdNUgKszYYedE7L32P9fSEG3x1phq0EbjeYkn5WpEi"
- name: alice
role: readonly
password: "$2a$10$sslrTYVwaIaA7O1lhokY2OgnojP5bB8YJ/o2MXaFP1v49lG8fqJYK"
default_role: readonly # fallback for users without an explicit role
KeyMeaning
rolesRole definitions: a name and a list of rules.
usersPrincipals bound to exactly one role, each with a bcrypt password hash or nopass: true.
default_roleOptional fallback role applied to any user without an explicit assignment.

A user without a password (nopass: true) accepts any password; a passwordless default user lets connections start authenticated and inherit its effective role. Every connection that cannot resolve a role is denied everything — RBAC is fail-closed by design.

A role’s rules are Redis-style tokens, applied in any order:

TokenEffect
+cmdGrant one command (e.g. +get)
-cmdRevoke one command
+@cat / -@catGrant / revoke a whole category
~prefixWhitelist a key namespace (e.g. ~users:*)
  • - rules always override + rules, regardless of order.
  • An empty whitelist — or an explicit ~* — allows every key.
  • If any namespace rule exists, only matching prefixes pass (default-deny).
  • A role with no rules is a valid deny-all role.

+@cat expands to the category’s registered commands at load time.

CategoryGrants
loginAUTH, PING, COMMAND
readGET, INFO
writeSET, DEL
readwriteread + write + login
operatorreadwrite + FLUSH
maintenanceFLUSH, SHUTDOWN, CONFIG, DEBUG, MONITOR
adminAUTH, ROLE, ACL, USER, GRANT, REVOKE
all / noneevery registered command / nothing

Passwords in the policy file are bcrypt hashes ($2a$10$...). Generate one with either tool:

Terminal window
htpasswd -nbBC 10 "" "PASSWORD" | tr -d ':\n' # apache2-utils
mkpasswd -m bcrypt "PASSWORD" # whois

ROLE SETUSER accepts a raw >password and bcrypt-hashes it server-side, so runtime-created users need no tooling. Full hash verification happens at the first AUTH; the hash itself is never rendered by ROLE LIST or ROLE GETUSER.

Instead of a password, a client can present an OIDC id_token at AUTH. The token is verified against a configured identity provider, its claims resolve to a role through the policy’s oauth.rules (first match wins), and the token’s sub becomes the session username. See OAuth / OIDC.

  • A connection’s role is resolved once at handshake and pinned for the connection’s lifetime. A later policy reload never changes an in-flight session.
  • Unauthenticated data commands return -NOAUTH; commands a user’s role does not grant return -NOPERM.
  • bcrypt verification runs on a dedicated worker pool off the event loop, so hashing never blocks I/O.

Authenticated users with the ROLE permission (the admin category) can manage roles and users without a restart:

CommandEffect
ROLE CREATE <name> <rule>...Define a new role; fails if it already exists (update = DELETE then CREATE)
ROLE SETUSER <user> <role> [>password] [nopass]Create or update a user; a >password or nopass option is required
ROLE DELUSER <user>Remove a user
ROLE DELETE <role>Remove a role; users referencing it fall back to the default role
ROLE LISTEnumerate roles with their granted commands and namespaces
ROLE GETUSER <user>Show a user’s role and whether a password is set

ACL LOG (see the ACL command page) returns the recent rejected-AUTH buffer: up to 100 entries, each a [timestamp, username, remote address, reason] tuple. The log lives on the RBAC store — not the policy snapshot — so it survives SIGHUP reloads, and it is shared by both the RESP2 and the native binary protocol. The store-wide failure counter behind it is the same one tellstone_rbac_auth_failures_total reports in Metrics.

Example over RESP2:

> ROLE CREATE operator +get '~users:*'
+OK
> ROLE SETUSER bob operator '>bobpw'
+OK
> ROLE GETUSER bob
bob / operator / 1

ROLE GETUSER returns the username, its assigned role (or null when the default role applies), and 1/0 for whether a password is set — the hash itself is never exposed. ROLE LIST returns each role’s name, granted commands, and namespace whitelist.

See the ROLE command reference for full details, including the native binary protocol.

Send SIGHUP to re-read the policy file and atomically publish the new snapshot. A rejected file (parse error, unknown role, bad hash) is ignored and the last valid policy stays active.

Because the reload replaces the whole policy with the file’s contents, any roles or users created at runtime via ROLE CREATE / ROLE SETUSER are discarded — persist them in the policy file first.

With --enable-metrics, RBAC counters are exposed on /metrics:

MetricTypeDescription
tellstone_rbac_auth_failures_totalcounterFailed AUTH attempts
tellstone_rbac_denied_commands_totalcounterAuthorization-denied command attempts (-NOPERM)
tellstone_rbac_commands_total{role="..."}counterData commands executed per role

A SIGHUP reload swaps in freshly parsed roles, which resets per-role command counts. Runtime ROLE CREATE does not.

A ready-to-run policy file and a walkthrough client ship in the main repository at cmd/example/role — start a server with --rbac-config, then run the example to watch an admin create a user, a reader hit its namespace gate, and denials surface as NOT_AUTHORIZED errors.