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-password is 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.json as 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.

RoleGrantsCovers
readreadget, exists, count, list, search, btree scans, blob read
readWriteread + writeput, insert, update, delete, batch put, blob write, queue/heap mutations
dbAdminread + write + dbAdmincreate/drop bucket, index changes, bucket stats, retention
userAdminuserAdmincreate/alter/drop users, grants, password changes
clusterAdminclusterAdminnode add/remove, rebalance, migration, gossip
backupbackup (+ read)dump/backup, including buckets it cannot read directly
restorerestorerestore/import
rooteverything, all databasesthe 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.