docs(ops): document dashboard password rotation — cmd/hashpw exists and is undocumented #44

Closed
opened 2026-08-06 13:57:27 +02:00 by bosun · 0 comments
Owner

The gap is documentation, not the tool

The operator asked whether the dashboard password lives in Vault, and whether a change-password form belonged in the dashboard. Both answers turned out to be no, and the tool that does the job already exists and is undocumented — neither he nor I knew about it until I read cmd/.

cmd/hashpw/main.go   reads stdin (NOT argv — deliberate: argv is visible in `ps`
                     to every process on the host and lands in shell history)
                     refuses passwords under 12 characters
                     prints the bcrypt hash purser expects
mentions in docs/    ZERO
mentions in README   ZERO

I proposed building it before I looked. That is the duplicate-check failure, on a feature rather than a tracker: the thing was already there, with the exact design rationale I had just argued for, written by whoever built it.

Why no change-password UI — worth recording so it is not re-proposed

secrets/dashboard-password-hash → /etc/purser/password-hash  :ro   READ-ONLY by design

A change form requires the container to write its own credential store. That makes a network-facing service that issues VPN client certificates able to rewrite its own auth. Three further reasons:

  • No user model. Sessions are in-memory (internal/web/auth.go:17-21, deliberately — "a stolen database yields live sessions as well as bundles"), and there is one shared password. A change form implies identities that do not exist.
  • It upgrades the wrong attack surface. A stolen session currently issues a cert. With a change form it also locks the operator out.
  • Rotation is rare — one operator, one credential.

The operator reached the same conclusion independently once the read-only mount was named.

Scope

Document the rotation procedure where an operator will find it — docs/operations.md alongside the 168h lifetime and 8760h ceiling notes, and a line in the README's operations section.

cd /srv/docker/purser/src
go run ./cmd/hashpw              # type the password at the prompt; it is not echoed to argv
                                 # → $2a$10$…
# replace the single line in /srv/docker/purser/secrets/dashboard-password-hash  (0600 alex:alex)
docker compose restart purser    # also invalidates existing sessions — they are in-memory

Points the docs must make, because each is non-obvious and each has a reason:

  • hashpw is not in the image. The Dockerfile builds only ./cmd/purser (Dockerfile:19-21,34), so this runs from the deploy tree at /srv/docker/purser/src, which sits at the released tag.
  • Do not pass the password as an argument to anything. That is the reason the tool reads stdin, and a doc that shows echo 'pw' | … teaches the habit the tool exists to prevent. Show the interactive form.
  • The restart is part of the procedure, not an afterthought — the hash is read at startup (internal/config/config.go:259) and sessions do not survive it.
  • The mount is read-only on purpose, with a pointer to the reasoning above so the change-password question resolves without re-litigating it.

Optional, and I would not do it without a reason

Shipping hashpw in the image would allow docker exec -i purser hashpw, removing the source-tree dependency. It also puts a second binary in a cert-issuing container for a task performed rarely from a host that already has the tree. Mentioned so the trade is recorded, not proposed.

Acceptance criteria

  • docs/operations.md carries the rotation procedure, in the interactive form
  • It states that hashpw is not in the image and why the invocation is from the source tree
  • It states that the restart is required and that it invalidates sessions
  • It records why the hash mount is read-only and why there is no change-password UI
  • README's operations section links to it

Anchor

Operator question, 2026-08-06, after the v0.3.0 deploy. Filed by Bosun. Reviewed reasoning: the read-only mount is the argument that settled it, and it is the one an operator cannot see from the dashboard.

## The gap is documentation, not the tool The operator asked whether the dashboard password lives in Vault, and whether a change-password form belonged in the dashboard. Both answers turned out to be *no*, and the tool that does the job **already exists and is undocumented** — neither he nor I knew about it until I read `cmd/`. ``` cmd/hashpw/main.go reads stdin (NOT argv — deliberate: argv is visible in `ps` to every process on the host and lands in shell history) refuses passwords under 12 characters prints the bcrypt hash purser expects mentions in docs/ ZERO mentions in README ZERO ``` **I proposed building it before I looked.** That is the duplicate-check failure, on a feature rather than a tracker: the thing was already there, with the exact design rationale I had just argued for, written by whoever built it. ## Why no change-password UI — worth recording so it is not re-proposed ``` secrets/dashboard-password-hash → /etc/purser/password-hash :ro READ-ONLY by design ``` A change form requires the container to write its own credential store. That makes a network-facing service that issues VPN client certificates able to rewrite its own auth. Three further reasons: - **No user model.** Sessions are in-memory (`internal/web/auth.go:17-21`, deliberately — *"a stolen database yields live sessions as well as bundles"*), and there is one shared password. A change form implies identities that do not exist. - **It upgrades the wrong attack surface.** A stolen session currently issues a cert. With a change form it also locks the operator out. - **Rotation is rare** — one operator, one credential. The operator reached the same conclusion independently once the read-only mount was named. ## Scope Document the rotation procedure where an operator will find it — `docs/operations.md` alongside the `168h` lifetime and `8760h` ceiling notes, and a line in the README's operations section. ```bash cd /srv/docker/purser/src go run ./cmd/hashpw # type the password at the prompt; it is not echoed to argv # → $2a$10$… # replace the single line in /srv/docker/purser/secrets/dashboard-password-hash (0600 alex:alex) docker compose restart purser # also invalidates existing sessions — they are in-memory ``` Points the docs must make, because each is non-obvious and each has a reason: - **`hashpw` is not in the image.** The Dockerfile builds only `./cmd/purser` (`Dockerfile:19-21,34`), so this runs from the deploy tree at `/srv/docker/purser/src`, which sits at the released tag. - **Do not pass the password as an argument** to anything. That is the reason the tool reads stdin, and a doc that shows `echo 'pw' | …` teaches the habit the tool exists to prevent. Show the interactive form. - **The restart is part of the procedure, not an afterthought** — the hash is read at startup (`internal/config/config.go:259`) and sessions do not survive it. - **The mount is read-only on purpose**, with a pointer to the reasoning above so the change-password question resolves without re-litigating it. ## Optional, and I would not do it without a reason Shipping `hashpw` in the image would allow `docker exec -i purser hashpw`, removing the source-tree dependency. It also puts a second binary in a cert-issuing container for a task performed rarely from a host that already has the tree. **Mentioned so the trade is recorded, not proposed.** ## Acceptance criteria - [x] `docs/operations.md` carries the rotation procedure, in the interactive form - [x] It states that `hashpw` is not in the image and why the invocation is from the source tree - [x] It states that the restart is required and that it invalidates sessions - [x] It records why the hash mount is read-only and why there is no change-password UI - [x] README's operations section links to it ## Anchor Operator question, 2026-08-06, after the v0.3.0 deploy. Filed by Bosun. Reviewed reasoning: the read-only mount is the argument that settled it, and it is the one an operator cannot see from the dashboard.
bosun closed this issue 2026-08-06 15:16:09 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/purser#44
No description provided.