Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8919,6 +8919,26 @@ That covers every startable topology, not just the loopback default. The one pos

**`scripts/dev/sqlserver-docker.ps1:64` re-measured and deliberately NOT fixed.** The literal is `'Str0ng_P@ssw0rd!'`, used when `MEFOR_STORE_PASSWORD` is unset and fed to `MSSQL_SA_PASSWORD` at `:191`. The same param block carries an explicit dev-only suppression justification, and whether `scripts/` is inside this cell's corpus is the unsettled scope question named above. Fixing it before that ruling moves nothing and would read as progress.

**BUILD 2026-09-05 (builder), NOT A CLOSURE -- this item stays OPEN and the cell stays PARTIAL.** What landed is the researched finding's *first* half: `messagefoundry provision-admin`, the offline first-administrator command, plus the decision record the 2026-09-04 pass recorded as owed ([ADR 0183](adr/0183-provision-the-first-administrator-offline-no-default-account-at-first-run.md), allocated atomically, index row in the same commit). `_ensure_bootstrap_admin` is untouched, so **the shipped default still mints an enabled account named `admin`** and the three characterization tests stay green. Nothing was written to the scorecard; the re-score is a separate act on a record that is not in this repository.

**THE ARM TAKEN IS "NOT PRESENT", AND IT IS NOW REACHABLE BY AN OPERATOR ACTION RATHER THAN A KNOB.** Run the command before the first `serve` and the account named `admin` never exists: the seeding path fires only on an empty user table, so an operator-named administrator pre-empts it. The way in is filesystem authority over the store, which is the same host gate `admin-unlock` ships on ([ADR 0171](adr/0171-offline-administrator-unlock-a-host-gated-cli-recovery-path-for-a-sole-administrator-lockout.md)) and is argued there rather than restated. Neither refused move was taken: `BOOTSTRAP_USERNAME` is unchanged, and `bootstrap_expiry_hours` is unchanged.

**THE CORRECTED GUARD IS THE ONE BUILT.** The refusal asks `has_enabled_administrator()`, not `count_users() == 0`. `tests/test_provision_first_administrator.py::test_the_guard_asks_for_an_administrator_not_an_empty_table` drives a completed directory sign-in, asserts the table is non-empty with no administrator, and then provisions -- the exact state the researched guard would have refused in. **That refusal is wider than the bootstrap guard by design and the ADR records it as a new standing affordance**, overlapping #1236's subject on the same host boundary. It was shipped rather than held for a ruling because a builder cannot ask and wait, and because the narrower alternative is measured to refuse where an install has no way in; if the owner rules the other way, the change is one predicate.

**THE HEADLESS NSSM RESTART ANSWER, WHICH IS THIS ITEM'S OWN NAMED RISK: there is no half-claimed state to restart into, by construction.** The design has no claim ceremony. The credential is typed at a TTY and `users.password_claimed_at` is stamped at creation, so the account is a normal administrator from birth. Two consequences. First, a restart -- headless or not -- sees either an install with a provisioned administrator or one without, never a partial ceremony. Second, WP-3 cannot reach it: an operator who names the account `admin` would otherwise satisfy `_unclaimed_bootstrap` and be auto-disabled at `bootstrap_expiry_hours`, **disabling the deployment's only administrator**. That is not hypothetical -- a planted control (`must_change_password=True` on the credential write) reds exactly two tests and the captured audit log shows `auth.bootstrap_admin_retired` firing on the just-provisioned account. The interrupted-run case is handled separately: the row is created with **no** password hash, then the credential, then the role, so every crash point leaves at most an enabled local account with no roles, which is denied everything by default, and a re-run with the same username completes it and reports `repaired`.

**A DRAFT OF THIS BUILD REPRODUCED THE ITEM'S OWN NAMED RISK, and it is recorded because the way it hid is the reusable part.** The draft's repair also refused a stamped `password_claimed_at` -- which is what the credential write sets -- so the SECOND interruption point left a row the command could never complete and WP-3 could never retire: a stranded install, reachable through the very code written to prevent one. It survived a green suite because the test named both half-written states in its docstring and exercised only the first, and it was caught by an adversarial reading of the write order rather than by any check. The discriminator is now **holds no roles**, one signal instead of a conjunction, chosen because it is the only one true at both points; the test is parametrized over both, and restoring the draft's refusal reds the second parameter and nothing else. **A conjunction of plausible-looking conditions is how a refusal acquires a state nobody enumerated.**

**TWO SMALLER CORRECTIONS FROM THE SAME REVIEW.** The M-31 substitute above read `[store].path`, which is the **SQLite** settings field: on Postgres or SQL Server it would have named a file the command never touched, so the only control covering the mistyped-target hazard would have misreported on two of three backends. It now reads the opened store's cross-backend `path` descriptor. And the gate message named the command as a remedy for the instance that had just refused -- where it declines, because by then an enabled Administrator exists. It is now phrased as a fresh-install instruction, which is the SDS-3.4 test applied to the one string the change touched.

**THE UNATTENDED ESCAPE HATCH IS REFUSED IN TERMS, which is the second unpriced cost the 2026-09-04 pass named.** There is no `--password` and no `--password-file`, asserted by a parser-level negative control rather than by the docstring that says so; with no terminal the command refuses and names why. The refusal is the decision, not an omission, so the next reader does not add the flag as an obvious convenience.

**CORRECTION (1) FROM THE 2026-08-20 WORK LIST IS APPLIED AS WRITTEN.** The PHI security-notice deliverability gate is **not** weakened -- only its message changed, to name the command and its `--email`. The CLI deliberately does **not** re-derive that gate's rule: it warns when no address is supplied and leaves the startup gate the single authority (SDS-3.5). `--email` lands on `notify_email`, the engine-owned column ([ADR 0182](adr/0182-split-the-account-mirror-address-from-the-engine-owned-notification-address.md)), on both the fresh and the repaired path.

**ONE CONTROL THIS COMMAND CANNOT CARRY, stated rather than papered over.** `admin-unlock` refuses a missing SQLite store so a typo'd `--db` cannot silently create one (M-31). This command **creates** the store -- that is the ordinary first-run case -- so the same guard is impossible. A mistyped `--db` therefore provisions into a store `serve` never opens, and `serve` mints the default account after all, with the command having reported success. The substitute is naming the resolved store path in the output. It is weaker and is recorded as weaker.

**WHAT REMAINS, AND IT IS WHAT DECIDES THE CELL.** Retiring `_ensure_bootstrap_admin` and the WP-3 lifecycle, at the cost the 2026-09-04 pass measured: `initialize()`'s 197 call sites across 64 files, the store protocol's three backends, `bootstrap_expiry_hours` / `bootstrap_warn_hours`, `_emit_bootstrap_admin`, `_bootstrap_expiry_reminder`, the `bootstrap_admin_expiring` alert, `tests/test_bootstrap_admin_perms.py`, six documents and the four IDE files. **That work is now unblocked rather than merely scoped**, because the replacement way in exists and is tested; the deletion no longer has to invent one under the stranding risk. Still open: `users.password_claimed_at` now has a second writer and reader outside the deleted lifecycle, which narrows but does not settle whether the column survives. Also still unsettled, and deliberately not acted on: whether `scripts/` is inside this cell's corpus, which is what `scripts/dev/sqlserver-docker.ps1:64` turns on.

## 1137. research an honest pass for ASVS 6.3.4 -- consistent authentication strength when AD MFA is delegated by design

> 🔢 **Re-scored 2026-08-20 -> P2.** Value **6/10** · Difficulty **8/10** · _big bet_. Both directory legs still mint sessions MFA-verified unconditionally while local accounts face a second factor as an access gate, and require_mfa_scope still defaults to every_local_account, so the strength gap is widest on the shipped default. Value 6 rather than 7 because the shipped comment at service.py:879-882 names a reachable out-of-engine control -- an MFA-enforcing LDAP proxy in front of the simple bind makes the directory pathway genuinely second-factor-backed -- so the residual gap is that the engine asserts rather than verifies, which is an awkward workaround and not the absence of one; difficulty 8 because the routes the research must weigh are directory-side evidence the engine can verify or an AD enrollment authority the code deliberately defers, both new architectural seams in auth. _(was 7/10 · 8/10.)_
Expand Down
26 changes: 26 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ auto-retirement is not the only way to lose an administrator: the failed-attempt
bootstrap login is refused like any other invalid credential and the retirement is audited
(`auth.bootstrap_admin_retired`).

### Provisioning the first administrator instead (ASVS 6.3.2)

**Run `messagefoundry provision-admin --username <name> --email <address>` before the first `serve`
and no bootstrap admin is ever created** — the seeding above fires only on an empty user table, so an
operator-named administrator pre-empts it and the account named `admin` never exists. That is the
"not present" arm of ASVS 6.3.2, and it is why the command exists
([ADR 0183](adr/0183-provision-the-first-administrator-offline-no-default-account-at-first-run.md),
BACKLOG #1136). **The shipped default is unchanged:** skip this and you still get the bootstrap
account described above.

Four properties are load-bearing rather than incidental:

- **The gate is host access**, the same one `messagefoundry admin-unlock` ships on
([ADR 0171](adr/0171-offline-administrator-unlock-a-host-gated-cli-recovery-path-for-a-sole-administrator-lockout.md)):
the service config, the store path and, on an encrypted store, the key material. Nothing is
reachable over the network.
- **The password is read from a terminal.** There is deliberately no `--password` and no
`--password-file`: either would put a standing Administrator credential in argv or on disk, so
unattended provisioning is refused rather than given a hatch.
- **It refuses when an enabled Administrator already exists** — not merely when the table is empty,
because a directory sign-in can fill the table without producing an administrator.
- **The credential is claimed at creation**, so the account is a normal administrator from birth and
WP-3 auto-retirement never applies to it, even under the name `admin`.

Re-running with the same username completes a provision an earlier run left half-written, and says so.

### Admin password reset (WP-L3-12, ASVS 6.4.6)

An administrator (`users:manage`) recovers a locked-out or compromised **local** account with
Expand Down
Loading
Loading