Pre-1.0 hoardDB is pre-1.0. Expect breaking changes.
User management
hoardDB authenticates a user per connection and authorizes every operation
against the roles that user holds. Users live in users.json beside the data
directory, survive restarts, and replicate to every node in a cluster.
This page is the operator’s view. It assumes a running server and a CLI session; getting-started.md covers both.
Where user changes run
Every command on this page is a mutation, and mutations are served by the
cluster master — the node that owns the reserved _system.auth key. On a
single node that is the node you are talking to. In a cluster, ask which node it
is:
[h]oardDB> master
cluster master for user changes: node-a at 10.0.0.4:7433
user list also prints the master and the current config version. A mutation
sent to any other node is refused with the master’s address; run the command
against that node.
The user store on disk
users.json sits in the data directory, mode 0600, and every node in a cluster
holds a full copy.
The file is encrypted at rest with AES-256-GCM under the node master key,
so a copy of the data directory does not hand out password hashes and cannot
be edited to inject a user or revive a disabled one. The node master key is
held by the keystore (see docs/user/configuration.md), not the data directory.
That makes the key part of the store’s durability, not only its security:
Back up the keystore configuration together with the data directory. A node that starts without the key material the store was written under refuses to start, naming the key it needs:
users.json is encrypted with key <kid>, but that key is not in the keystore — restore the original keystore configuration or lose the users--reset-passwordis not a recovery path for that case. It reads the same file and fails the same way; it can only rewrite a store it can already read.Decommissioning a node means deleting its
users.jsonas well as wiping its cluster token — the copy is readable by that node and by anyone holding its key material.
A users.json written before this encryption existed is plain JSON and still
loads as-is: the format is detected from the file itself, so there is no
migration step and no flag to set.
Create a user
Passwords are prompted, never passed as a flag. On a terminal the CLI asks twice and does not echo:
[h]oardDB> create user alice roles [readWrite@app, read@logs]
New password:
Repeat new password:
user "alice" created (config version 2)
For scripts, read the password from a 0600 file or from stdin:
hoardDB --password-file /run/secrets/alice.pw # create user alice roles [...]
hoardDB --password-stdin < /run/secrets/alice.pw # same
There is no --password flag on any user command: a password on a command line
is visible in ps and shell history, and a pre-hashed credential is an
offline-crackable artifact of a password people reuse. The server never receives
the plaintext on this path — the CLI derives the Argon2id hash locally and sends
only the salt, hash and parameters, which the server peppers before storing.
Roles
Roles are named after MongoDB’s, so the intuition transfers. A role is always
bound to a database: readWrite@app grants read and write on app. Use db: "*" (readWrite@*) for every database.
| Role | Grants | Covers |
|---|---|---|
read | read | get, exists, count, list, search, btree scans, blob read |
readWrite | read + write | put, insert, update, delete, batch put, blob write, queue/heap mutations |
dbAdmin | read + write + dbAdmin | create/drop bucket, index changes, bucket stats, retention |
userAdmin | userAdmin | create/alter/drop users, grants, password changes |
clusterAdmin | clusterAdmin | node add/remove, rebalance, migration, gossip |
backup | backup (+ read) | dump/backup, including buckets it cannot read directly |
restore | restore | restore/import |
root | everything, all databases | the bootstrap administrator |
Two notes on scope. A role only applies to the database it was granted on:
readWrite@app + read@logs can write app and cannot write logs. A user
with no grant on a database is refused there. And user and cluster
administration are store-wide in v1 — the database in a userAdmin or
clusterAdmin grant is not used to narrow the action.
Read users back
[h]oardDB> user list
USERS (master: node-a at 10.0.0.4:7433, config version 2)
---
admin: enabled, roles [root@*], epoch 0
alice: enabled, roles [read@logs, readWrite@app], epoch 0
[h]oardDB> user describe alice
user: alice
status: enabled
roles: [read@logs, readWrite@app]
epoch: 0
created: 2026-09-19T06:45:01Z
password changed: 2026-09-19T06:45:01Z
user list and user describe never show a hash, a salt or a pepper.
Change a user
Grant and revoke are incremental:
[h]oardDB> grant role dbAdmin@logs to alice
granted dbAdmin@logs to "alice" (config version 3)
[h]oardDB> revoke role dbAdmin@logs from alice
revoked dbAdmin@logs from "alice" (config version 4)
alter user ... roles [...] replaces the whole role set, so it is refused if
the user store changed since you read the version (the CLI reads the current
version for you). Prefer grant/revoke when you are adding or removing one role:
[h]oardDB> alter user alice roles [read@app, readWrite@logs]
user "alice" altered (config version 5)
Disable and enable:
[h]oardDB> alter user alice disable
user "alice" altered (config version 6)
[h]oardDB> alter user alice enable
user "alice" altered (config version 7)
Change a password (prompted, or --password-file / --password-stdin):
[h]oardDB> alter user alice password
New password:
Repeat new password:
user "alice" altered (config version 8)
A password change bumps the user’s epoch, which refuses every token minted
before it at the next refresh. The old password stops working immediately.
Drop a user
[h]oardDB> drop user alice
user "alice" dropped
The last enabled userAdmin or clusterAdmin cannot be dropped, disabled, or
stripped of the granting role; hoardDB refuses and names the reason. A lockout is
not recoverable through the CLI.
What happens to an open connection
- Role changes, grants and revokes apply at the user’s next AUTH. An already authenticated connection is not terminated and keeps the roles it authenticated with; a token refresh picks up the narrowed or widened set.
- Disable and drop close that user’s existing sessions immediately. The next frame on those connections fails; the client must reconnect (and will be refused).
Reading a refusal
A permission refusal names the user, the missing action, the resource, the roles the user actually holds, and the command that fixes it:
[h]oardDB> create database other
CLI error: create database "other": server error (5): permission denied: user "alice" lacks "dbAdmin" on "other"
alice's roles: read@app
ask an administrator to run: grant role dbAdmin@other to alice
Repeated identical refusals reach the audit log once per minute with a count of the repeats after that, so a misconfigured client in a retry loop does not flood the log.
Source: docs/user/user-management.md in the repository.