Files
OpenPXE/crates
Miles Ward 9c6903351f v0.3.1: per-ISO boot password gate
Operators can now lock individual ISOs behind a password set in the
WebUI. Picking a locked image at the PXE menu prompts the operator on
the client console; the boot script is only released after a correct
match. The plaintext never leaves the request — server stores bcrypt
hashes, scripts never echo the candidate.

## Backend

- New optional `password_hash: Option<String>` on `IsoMeta`. Skipped
  during serialize when None, so existing meta.json files don't grow
  a noisy `null` field.
- `IsoStore::set_password(id, Some("pw"))` hashes via bcrypt
  `DEFAULT_COST` (10 — fast enough for an interactive iPXE prompt,
  expensive enough to be hostile to brute force on a leaked
  meta.json). `set_password(id, None)` and `set_password(id, Some(""))`
  both clear.
- `IsoStore::verify_password` returns Ok(true) when no password is
  set, so the gate stays open for the common case.
- `IsoMeta::is_password_protected()` predicate the HTTP layer + UI
  share.
- NFS-sourced ISOs persist their hash in memory only — the share is
  the source of truth for those, and it doesn't carry hash sidecars.

## HTTP API

- `PUT /api/isos/:id/password` body `{ "password": "..." }` to set,
  `{ "password": null }` (or empty string) to clear.
- `DELETE /api/isos/:id/password` for the explicit clear.
- Both 204 on success, 404 for unknown ids.
- `/boot/<entry>.ipxe` now intercepts:
  - no `?token=`        -> render password-prompt script
  - `?token=<wrong>`    -> render auth-fail script (sleeps 2s, chains
                           back to the entry which re-prompts)
  - `?token=<correct>`  -> render the real boot script
  - ISO without password ignores token entirely (per-MAC bookmarks
    still work without changes).

## iPXE prompt

`render_password_prompt`:
- `set password ` then `read --secret password` — accepts input
  without echoing.
- Empty input chains back to the main menu (lets the operator back
  out of a misclick).
- Submit chains `?token=${password:uristring}`. The `:uristring`
  modifier URL-encodes the value, so passwords with `&`, `?`, `=`,
  spaces, etc. survive transport.

`render_password_failed`:
- Single line saying so + 2s sleep, then re-chains the entry.
- Server-side WARN log records the entry id only, never the
  candidate value (verified in smoke test).

## UI

Storage tab's image table grows an `Auth` column showing
`protected` / `open`, plus a 🔒 next to the filename when locked.
Per-row "Set password" / "Password ✎" button toggles an inline
editor in the next table row containing:
- a "Password protect this image" checkbox
- a `<input type=password autocomplete=new-password>` (hidden when
  the checkbox is off)
- a Save button

Save calls PUT or DELETE on `/api/isos/:id/password` based on the
checkbox state and clears the input field before re-rendering, so
the plaintext doesn't sit in the DOM longer than needed.

## Menu indicator

`render_family_menu` adds a `*` prefix immediately before the size
box on protected entries — ASCII only because some firmware menu
consoles mangle non-ASCII glyphs. Looks like:

  item --key 1 win11_test-winpe *[ 5234 MB] Windows 11 Test ISO

## Tests

74 passing across the workspace (was 66 in v0.3.0):
- 3 new store unit tests (bcrypt round-trip, unknown-id error,
  meta.json persistence across restart)
- 2 new ipxe_script unit tests (prompt/auth-fail invariants:
  read --secret, uristring, no candidate echo)
- 3 new HTTP integration tests (full gate flow upload-set-prompt-
  fail-success-clear, null/empty bodies, 404 on unknown id)

cargo clippy --workspace --all-targets clean.

Local smoke verified upload + lock + prompt + auth-fail + correct +
menu indicator + log scrub on a real release binary.

## Operational notes

- HTTP, not HTTPS — token rides in the query string. Acceptable on
  a trusted boot VLAN; do NOT expose OpenPXE to untrusted networks
  with this feature relied on for security. Reverse-proxy in front
  of OpenPXE will end up with the token in access logs.
- bcrypt cost is `DEFAULT_COST` (10). One verify takes ~50ms on
  modern x86, which is the worst-case latency added to a correct
  boot. Tunable via the bcrypt crate if needed.
2026-05-06 22:35:11 -04:00
..
2026-05-06 22:35:11 -04:00