Admin Password Recovery
Ontul ships with a built-in recovery channel that lets an operator reset the
admin user's password without stopping the master, even when no one
remembers the current password.
Recovery uses a local Unix domain socket — there is no HTTP back-door, no network endpoint, no recovery URL. Authentication is performed by the operating system: only a process that already shares the master's filesystem identity can open the socket.
When to use
- The admin password was forgotten or rotated out of the password manager.
- An automation script needs to provision a known admin password during first-time setup, without going through the web UI.
- A new operator is onboarded and you need to hand them an admin credential.
For day-to-day password changes (the user remembers the old password and wants a new one), use the Admin UI's Change Password screen instead — that path requires the old password and does not flag the user for forced rotation.
How it works
┌────────────────────┐ JSON over UDS ┌────────────────────┐
│ ontul-cli │ ─────────────────▶ │ Master (running) │
│ iam:reset-password│ │ AuthManager │
└────────────────────┘ │ .adminResetPwd() │
▲ │ │
│ stdout: new password │ saveToDb() │
│ (one-time) │ cluster sync push │
│ audit log append │
└────────────────────┘
| Property | Value |
|---|---|
| Socket path | ${ontul.base.data.dir}/admin.sock by default, mode 600 — the live value is published to bin/master.socket |
| Authentication | OS file permission — same user as the master process |
| Socket path marker | bin/master.socket — written when the socket binds, removed on shutdown |
| Network surface | none — Unix domain socket only |
| Downtime | none — applied in-process on the live master |
| Cluster sync | automatic — leader pushes the new state to followers |
| Audit log | data/iam-audit/reset.log (mode 600, append-only) |
| Post-reset state | requirePasswordChange = true (forced rotation on next login) |
Quick start
The simplest invocation lets the master generate a strong 20-character
password and print it to stdout. The new password must be changed on the
admin's next login (the requirePasswordChange flag is set automatically).
Sample output (TTY):
┌────────────────────────────────────────────────────────────┐
│ Password reset for user: admin │
│ │
│ New temporary password: K3p9WvTx7qLm8zXa#-Bd │
│ │
│ Must be changed on next login (requirePasswordChange). │
└────────────────────────────────────────────────────────────┘
Input modes
| Mode | Command | When to use |
|---|---|---|
| Master-generated | ontul-cli.sh iam:reset-password |
Default. Strong random password printed once on stdout. |
| Explicit | ontul-cli.sh iam:reset-password --new-password 'My!Pass' |
Automation that knows the desired value. Beware: argv may show up in ps. |
| Stdin | echo 'My!Pass' \| ontul-cli.sh iam:reset-password --new-password - |
Automation that wants to avoid argv exposure. |
| Interactive | ontul-cli.sh iam:reset-password --interactive |
Operator at a TTY. Prompts for password twice with no echo. |
Resetting a different user is also supported:
Configuration
The recovery socket is enabled by default. Every key below lives in
conf/ontul.properties and is read at master startup:
# conf/ontul.properties
# Set false to remove the local recovery path entirely.
ontul.admin.socket.enabled = true
ontul.admin.socket.path = ${ontul.base.data.dir}/admin.sock
# Name of the file under <ontul.home>/bin that receives the socket path the
# master actually bound to (see "How the CLI finds the socket" below).
ontul.admin.socket.marker.file = master.socket
# Append-only audit trail of socket operations.
ontul.iam.audit.dir = ${ontul.base.data.dir}/iam-audit
The socket follows ontul.base.data.dir. If you launch the master with
-Dontul.base.data.dir=/var/lib/ontul the socket moves to
/var/lib/ontul/admin.sock — you do not have to restate it.
How the CLI finds the socket
Re-deriving the socket path from conf/ontul.properties is not reliable on its own:
ontul.base.data.dir can be overridden with -D at launch or edited after
startup, and the file does not record which value the live process used. So the
master publishes the path it actually bound to into
<install dir>/bin/master.socket when the socket comes up, and removes that file on
shutdown. bin/ontul-cli.sh prefers it.
Full resolution order, highest priority first:
--socket /path/to/admin.sock— read by the Java CLI, always wins.$ONTUL_ADMIN_SOCKET— if already exported in the caller's shell.<install dir>/bin/master.socket— the path published by the running master. Used only when the file exists and the path in it is a live socket.ontul.admin.socket.pathfromconf/ontul.properties, with${ontul.base.data.dir}expanded. A value that still contains a${...}placeholder is rejected rather than used literally.<install dir>/data/admin.sock, then/data/admin.sock.
Step 3 is what makes a moved data dir work: with the socket at
/data/admin.sock and the properties file still saying ./data, only the marker
knows where to connect.
To rename the marker, change one key — both ends read it:
Restart the master; it publishes bin/ontul-recovery.socket, and the CLI
picks the new name up from the same properties file.
The master key is for the master, not the CLI
ONTUL_MASTER_KEY must be exported for the master process. The start script does not pre-check it, but KMS is enabled by default and the master cannot unseal its keystore without the key, so startup fails during KMS initialisation.
The variable name itself is configurable — ontul.kms.master.key.env in
conf/ontul.properties names the variable the master reads:
bin/ontul-cli.sh does not need it. The CLI only opens the Unix socket and
hands the request to the running master, which already holds the unsealed key,
so this works with the variable unset:
If a CLI invocation complains about the key rather than the socket, you are running a start script, not the CLI.
Worked examples
# 1. On the host, as the same OS user that runs the master:
cd /opt/ontul
bin/ontul-cli.sh ping
bin/ontul-cli.sh iam:reset-password
# 2. The master runs as a service account and you are root:
sudo -u ontul /opt/ontul/bin/ontul-cli.sh iam:reset-password
# 3. Inside a container:
docker exec -it ontul-master-1 /app/bin/ontul-cli.sh iam:reset-password
# 4. Data dir was relocated at launch — no extra flags needed, the CLI
# reads the published marker:
cat /opt/ontul/bin/master.socket
# /var/lib/ontul/admin.sock
bin/ontul-cli.sh ping
# 5. Socket in a non-standard place and no marker (the master is stopped,
# or you are on a host where the marker was cleaned up):
bin/ontul-cli.sh --socket /var/lib/ontul/admin.sock iam:reset-password
# 6. Non-interactive automation, password from stdin so it never reaches argv:
echo 'S0me!Strong!Pass' | bin/ontul-cli.sh iam:reset-password --new-password -
Security model
1. The socket is OS-gated.
At startup the master creates data/admin.sock with mode 600 (owner
read/write only). Even other unprivileged users on the same host cannot
connect. There is no token, no shared secret, no network listener.
2. The audit log records every reset.
Every successful reset appends a JSON line to data/iam-audit/reset.log
(mode 600). The plaintext password is never logged — only the first
8 characters of its hash, the user, whether it was master-generated, and
the OS user that invoked the CLI.
{"ts":"2026-05-23T16:00:46.922Z","event":"iam.reset-password","user":"admin","generated":true,"hashFp":"OYV/Ojf/","invokedAs":"root"}
3. The new password is exposed exactly once. For master-generated passwords, the plaintext is returned only on the single CLI invocation that triggered the reset. It is not written to the audit log, not stored in RocksDB, and not retransmitted. Treat scrollback and shell history accordingly — or use stdin input mode to avoid argv exposure entirely.
4. Forced rotation on next login.
After reset, the user is flagged requirePasswordChange = true. The next
successful login forces the user through the change-password flow, so a
temporary password used by the operator is immediately replaced by a
password only the user knows.
5. The master must be running. Because the recovery channel is in-process, the master must be alive for the CLI to connect. This is intentional: RocksDB requires an exclusive lock, so an offline edit would either conflict with a running master or need a complex stale-lock recovery. With this design, the only way to reset is to be on the host and have the master running and share its filesystem identity.
Limitations
- Master must be running. If the master is down (e.g. KMS unseal failure during boot), this CLI cannot help. The recovery path in that case is to inspect the boot failure, fix the underlying issue, and let the master come up — then run the CLI.
- No knowledge factor. Any process that shares the master's filesystem identity can invoke the CLI. In multi-tenant or shared-shell environments, restrict shell access to the master accordingly. A future enhancement may add an opt-in "recovery key" requirement for an additional knowledge factor.
Related
- IAM — users, groups, policies
- High Availability — cluster sync of IAM state
- Encryption & KMS — how IAM is encrypted at rest