Add a webhooker resetpw subcommand and a bootstrap banner (closes #208) (#239)
All checks were successful
check / check (push) Successful in 3m25s

The admin bootstrap password was printed once, as one line among roughly
45 fx lines, and under docker run -d went to container logs subject to
rotation. There was no reset path at all -- no subcommand, no forgot-password
flow, no env override -- so recovery meant hand-deleting the users row from
webhooker.db, which was documented nowhere.

Adds webhooker resetpw [-generate] <username>. The password is read from
stdin or generated with the existing crypto/rand helper, never taken from
argv where /proc would publish it. It reuses the existing Argon2id hashing
rather than reimplementing the parameters, and writes a single UPDATE only
after the hash is complete, so no failure can leave an account with no
usable password. An unknown username is a hard error and never creates an
account.

It refuses to run against a DATA_DIR held by a live instance, via the
exclusive lock from #201. DATA_DIR and webhooker.db are checked to exist
before the lock is acquired, so a mistyped path creates nothing -- neither
a directory tree nor a stray lock file.

The bootstrap password now appears exactly once, in a distinct banner
written straight to a caller-named writer rather than as an fx log line.
This commit was merged in pull request #239.
This commit is contained in:
2026-08-20 08:01:42 +02:00
parent fcead5d401
commit 9969694a47
10 changed files with 1431 additions and 18 deletions

104
README.md
View File

@@ -286,9 +286,84 @@ On first startup, webhooker automatically generates a cryptographically
secure session encryption key and stores it in the database. This key
persists across restarts — no manual key management is needed.
On first startup, webhooker creates an `admin` user
with a randomly generated password and logs it to stdout. This password
is only displayed once.
#### The admin account
On first startup — a `DATA_DIR` with no accounts in it — webhooker
creates an `admin` user with a randomly generated password and prints
it to standard output as a ruled banner:
```
========================================================================
WEBHOOKER FIRST BOOT: an admin account has been created.
username: admin
password: 3xamPl3-p4ssw0rd
Save this password now: it is shown only here, and only once.
If it is lost, run `webhooker resetpw admin` on a stopped deployment.
========================================================================
```
It is a banner rather than a log line because that is the only time it
is ever shown: as one `INFO` record it sat among the roughly 45 fx
`PROVIDE`/`RUN`/`HOOK` lines a boot writes, and under `docker run -d`
it is one line in a log subject to rotation. The database stores only
its Argon2id hash. There is no second account and no forgot-password
flow, so the banner and the reset command below are the only two ways
in.
#### Recovering a lost admin password
`webhooker resetpw` sets an existing account's password from the
command line:
```bash
# Generate a new password and print it.
DATA_DIR=/var/lib/webhooker webhooker resetpw -generate admin
# Or supply one on standard input (minimum 8 characters).
printf '%s' "$NEW_PASSWORD" | \
DATA_DIR=/var/lib/webhooker webhooker resetpw admin
```
In a container it is the same binary, which the image sets as `CMD`
rather than `ENTRYPOINT`, so the whole command has to be given:
```bash
docker run --rm -v webhooker-data:/var/lib/webhooker \
webhooker /app/webhooker resetpw -generate admin
```
Stop the service first — with the volume still attached to a running
container, the command refuses.
The password is never taken as a command-line argument: on Linux argv
is readable through `/proc` by every account on the host for as long as
the process lives. Standard input is echoed when it is a terminal — the
prompt says so — so `-generate` or a pipe is preferable on a shared
machine.
What it will not do:
- **Run against a live deployment.** It takes the same exclusive
`DATA_DIR` lock the server does (see
[Single-instance lock](#single-instance-lock)) and refuses while a
running instance holds it, naming the directory and exiting non-zero.
A running process keeps serving every session that authenticated with
the old password, so a reset underneath it would report a change the
service does not honour.
- **Create anything.** A `DATA_DIR` that does not exist, or that holds
no `webhooker.db`, is an error rather than a new empty deployment —
a mistyped path must not be built out and then reported as a success.
- **Create an account.** A username that does not exist is an error.
`resetpw` changes an existing account's password and nothing else.
`DATA_DIR` selects the deployment exactly as it does for the server. A
password that changes on disk takes effect at the next login; sessions
that are already authenticated are unaffected either way.
Changing a password you still know needs none of this — use
`POST /user/{username}/password` in the web UI.
#### What `DEBUG=true` exposes
@@ -325,11 +400,14 @@ What it does **not** put in the log:
What is in the log regardless of `DEBUG`, and is not a debug-logging
decision:
- **The initial `admin` password**, in the clear, once, at `INFO`, on
the first boot that creates the account. That line is the only place
- **The initial `admin` password**, in the clear, once, on the first
boot that creates the account — as the banner described under
[The admin account](#the-admin-account), written straight to standard
output rather than through the logger. That banner is the only place
it is ever shown; the database stores the hash. A first boot's output
is not safe to paste anywhere until that account's password has been
changed.
changed. The same applies to `webhooker resetpw -generate`, which
prints the password it generated in the same form.
- **An authenticated operator's own configuration**, echoed back
untruncated — webhook names, target hostnames. See the logging
section under Security for the full list and for the per-line size
@@ -842,7 +920,10 @@ A registered user of the webhooker service.
Passwords are hashed with Argon2id using secure defaults (64 MB memory,
1 iteration, 4 threads, 32-byte key, 16-byte salt). On first startup,
an `admin` user is created with a randomly generated 16-character
password logged to stdout.
password printed once to stdout; `webhooker resetpw` sets it again if
it is lost (see [The admin account](#the-admin-account)). Every one of
those paths hashes through the same `internal/database` code, so the
parameters cannot drift between them.
#### Webhook
@@ -1994,8 +2075,12 @@ imports. The entry point is `cmd/webhooker/main.go`.
```
webhooker/
├── cmd/webhooker/
│ └── main.go # Entry point: sets globals, locks DATA_DIR, wires fx
│ └── main.go # Entry point: subcommand dispatch; no args locks DATA_DIR and wires fx
├── internal/
│ ├── banner/
│ │ └── banner.go # Ruled block for the one credential shown in the clear
│ ├── resetpw/
│ │ └── resetpw.go # `webhooker resetpw`: set an account's password, stopped deployments only
│ ├── config/
│ │ └── config.go # Configuration loading from environment variables
│ ├── database/
@@ -2191,6 +2276,9 @@ check, see [The login endpoint](#the-login-endpoint).
header. API keys are stored per-user with usage tracking
(`last_used_at`).
- **Metrics:** Basic authentication protecting the `/metrics` endpoint.
- **Recovery:** `webhooker resetpw <username>` on a stopped deployment
is the only way back into an account whose password was lost (see
[Recovering a lost admin password](#recovering-a-lost-admin-password)).
### Security