Add a webhooker resetpw subcommand and a bootstrap banner (closes #208)
Some checks failed
check / check (push) Failing after 2m32s
Some checks failed
check / check (push) Failing after 2m32s
The bootstrap admin password was shown exactly once, as one INFO record among the roughly 45 fx lines a boot writes, and there was no reset path at all: no subcommand, no forgot-password flow, no override. Losing that line meant deleting the users row from webhooker.db by hand so the next start would re-seed. - internal/banner renders the one credential shown in the clear as a ruled block written straight to standard output, so it does not read as one more log line. The first boot emits the password there and nowhere else, and the banner names the recovery command. - `webhooker resetpw [-generate] <username>` sets an existing account's password. It reads the password as one line from standard input, or generates one with crypto/rand via the existing GenerateRandomPassword; it is never an argv value, which /proc would publish to every account on the host. Hashing goes through database.HashPassword, so the Argon2id parameters cannot drift. - It refuses to run against a DATA_DIR a live instance holds, by taking the same exclusive flock internal/datadir gives the server, and releases it when it finishes. - It creates nothing. A missing DATA_DIR, a directory with no webhooker.db, and an unknown username are each an error: datadir .Acquire calls os.MkdirAll, so a mistyped path would otherwise be built out and reported as a success. The existence checks therefore run before the lock is taken. - The account is resolved and the hash computed in full before the single UPDATE that stores it, so any failure leaves the stored credential untouched. - database.Open exposes the connect-and-migrate path without fx and without seeding; seeding moves to ensureAdminUser, which only a server start calls. - main gains subcommand dispatch. No arguments still runs the server on the same path, with the DATA_DIR lock taken before the fx graph is built and fx owning the non-zero exit; an unknown subcommand exits 2 rather than starting a server. Tests: reset then log in through the real form POST handler, the generated password verifying against the stored hash, the refusal against a held lock, both create-nothing cases, the unknown user, the unusable passwords, and the first-boot banner carrying a password that opens the account. README documents the bootstrap banner and the recovery command, including the container invocation and what resetpw will not do.
This commit is contained in:
104
README.md
104
README.md
@@ -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
|
||||
@@ -725,7 +803,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
|
||||
|
||||
@@ -1866,8 +1947,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/
|
||||
@@ -2060,6 +2145,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user