Skip to content
Open
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: 19 additions & 1 deletion docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,8 @@ carries no open network exposure. The gaps below are **by design and tracked**
- **MFA is built but off by default** — native RFC 6238 TOTP for local accounts (WP-14, #336/#338), enabled
per deployment via `[auth].require_mfa`. Still single-factor until switched on, and the factor is TOTP
(shared-secret, replayable within its ~30 s step window) — phishing-resistant WebAuthn/FIDO2 is the WP-14b
follow-up. AD/Kerberos MFA is delegated to the directory.
follow-up. Directory accounts are in scope too (BACKLOG #1144) -- the delegation this line used to
claim is retired.
- **Off-box log shipping is built but opt-in** — the structured-JSON + syslog/SIEM forwarder + cross-backend
`audit_log` off-box tee shipped (sec-offbox-log #357/#361/#363); enabling it + pointing at a SIEM endpoint
is the per-deployment step, and native TLS-syslog (vs a local TLS-forwarding agent) is the residual.
Expand Down Expand Up @@ -8424,6 +8425,8 @@ filing.
> 🔢 **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.)_
> Research: done 2026-08-20
>
> PREMISE MOVED 2026-09-03 by BACKLOG #1144, which is a different item and does not close this one: the constraint this row states as fixed -- that the engine does not enroll a second factor for directory identities, so flipping the literal would confine every directory administrator -- no longer holds. The enrollment path was built and the Kerberos leg now mints at the minimum. Re-read #1144's BUILT paragraph before re-scoring or researching this row; the sentence about both directory legs minting MFA-verified unconditionally is stale on both halves.
>
> **Filed 2026-08-08 - not started. RESEARCH item: the goal is an HONEST pass, and "cannot honestly reach pass" is a valid finding.** ASVS **6.3.4** (L2) currently scores **partial**. The pinned verb asks that there be no undocumented pathways and that controls and authentication strength be enforced *consistently* across them. The AD and Kerberos legs mint sessions MFA-verified unconditionally (`messagefoundry/auth/service.py:794`, `:824`) while local accounts face a second factor as an access gate (`messagefoundry/api/security.py:227`).

> **SHIPPED-BUT-OPEN 2026-08-22 -- PR #497, ledger pairing authored by the LANDER per [ADR 0165](adr/0165-a-builder-pr-satisfies-the-ledger-gate-with-a-paired-commit-authored-by-the-dispatcher-or-lander.md); the builder did not author this.** Layer 1 splits `ad_enabled` into bind capability vs password-login pathway, and classifies `ad_password_login_enabled` as a non-input. Landing in that PR: `messagefoundry/auth/service.py`, `messagefoundry/config/settings.py`, `tests/test_ad_login_pathway_split.py`, `tests/test_security_posture_defaults.py`.
Expand Down Expand Up @@ -8734,6 +8737,7 @@ filing.

> 🔢 **Re-scored 2026-08-20 -> P2.** Value **6/10** · Difficulty **5/10** · _quick win_. Gap stands on both axes: auth_time appears nowhere in the auth tree, flow.py requests no max_age, and service.py:884/:914 still mint mfa_verified=True on the directory legs with no IdP evidence, so a directory session would satisfy the step-up gate on a first deployment (value 6). The remainder prices at 5, not 6: the AD arm is deferred to #296 by the item's own text, so the deliverable is a research finding plus at most an OIDC-leg recency check, a setting and a SECURITY.md fallback, with the store limb bounded to one nullable column on a sessions table that already gained reauth_at the same way (store.py:3181). _(was 6/10 · 7/10.)_
> Research: done 2026-08-20
> Build: steps 1 and 2 landed 2026-09-03 (directory-account engine-factor enrollment, and the Kerberos leg minting at the minimum, co-landed). Steps 3 through 6 stay open, so does this item. Read the BUILT paragraph before re-scoring: every line anchor in this row had drifted, one of the two directory legs it names was retired by #1137 before the build, and the deciding question the ceiling turns on is still untaken.
>
> **Filed 2026-08-08 - not started. RESEARCH item: the goal is an HONEST pass, and "cannot honestly reach pass" is a valid finding.** ASVS **6.8.4** (L2) currently scores **partial**. The pinned verb asks that where the application expects specific authentication strength, method or recentness for specific functions, it verifies that from what the IdP returned -- and where the IdP returns nothing, that a documented fallback assumes the MINIMUM mechanism. Recentness is unimplemented (`auth_time` appears nowhere in the auth tree; `build_authorization_url` at `auth/oidc/flow.py:193` sends no `max_age`), and on both directory legs the engine mints `mfa_verified=True` on no IdP evidence at all.
> Verdict: research
Expand All @@ -8760,6 +8764,20 @@ filing.

**THE BUILD, under the narrower reading, in dependency order.** (1) **Directory-account engine-factor enrollment**: delete the four non-local refusals (`messagefoundry/auth/service.py:2112/:2130/:2378/:2412`) and the blanket strength exemption (`:2060-2061`). (2) **Mint at the minimum on both directory legs** (`:884`, `:911-914` -- the shipped comment at `:883` already names the parameter as the seam), **co-landed** with (1): with zero deployments the upgrade-lockout cost is vacuous, but shipping it alone is the measured confinement, since a minimum-minted directory session is refused on every route outside the six-entry exempt set at `messagefoundry/api/security.py:79-88` and neither enrollment ceremony accepts a non-local account. (3) **Federated recentness as one indivisible unit**: add a maximum-age parameter to the authorization request (`messagefoundry/auth/oidc/flow.py:206-220` builds nine fixed parameters and no such value), validate the authentication-time claim beside the gate at `messagefoundry/auth/oidc/claims.py:291-310`, **refuse** when it was requested and the claim is absent or stale, and cap the session accordingly. Building the request half alone is the ask-and-never-check shape the shipped assurance-class settings already exhibit (`messagefoundry/config/settings.py:1983` ships empty while `:1984` is requested). (4) A **federated step-up leg**, which needs a session mechanism discriminator and needs no directory enrollment -- the one arm this item asks about directly. (5) **Stop seeding the step-up window at login on the directory legs**, matching the console SSO route, or per-function binding there stays satisfied by a self-stamped timestamp; sequence it against (1), because a second step-up factory (`messagefoundry/api/security.py:577-608`, gating session termination at `messagefoundry/api/auth_routes.py:481` and `:493`) is not covered by the action-step-up requirement. (6) **Then, and only then**, the minimum-strength fallback statement in the documentation -- written today it would be false, which is this item's own trap. **What no build reaches:** strength and method on the simple-bind and SSO legs, whose protocols return no assertion; the verb routes those to the fallback clause, so any future record claiming they **verify** strength is wrong on the protocol. **A NEW CHEAP MOVE the 2026-08-16 ruling created:** arguing that on the shipped default no separate provider exists, so the conditional verb is vacuously satisfied. Rejected by name, and not-applicable is separately foreclosed because the precondition is reachable by configuration.

**BUILT 2026-09-03: steps 1 and 2 landed, co-landed, on the owner's ruling of that date. Steps 3 through 6 stay open, so this item stays open.** The owner did not resolve the deciding question below and ruled instead that steps 1 and 2 are worth funding on the merits whichever way it eventually goes.

**What landed.** (1) The engine's second factor is now reachable by a DIRECTORY account: the non-local refusals came out of `begin_mfa_enrollment`, `confirm_mfa_enrollment`, `begin_webauthn_registration` and `finish_webauthn_registration`, the blanket provider exemption came out of `_mfa_required_for`, and the `_externally_managed` refusal came off `POST /me/mfa/enroll` in `api/auth_routes.py`, which is the only route reaching the ceremony over HTTP. (2) `authenticate_kerberos` passes `mfa_verified=False`, so a directory session is minted MFA-pending. Two sites the item's enumeration did not name were included and are called out rather than buried: `admin_reset_mfa`'s non-local refusal, without which enrollment is a one-way door for a directory user who loses an authenticator, and the console account page, which hid the MFA and passkey cards from a directory account and would have stranded the users the mint confines. `mfa_satisfied`'s directory branch is now a floor rather than an exit: with `require_mfa` on it refuses, and with it off it falls through to the shared per-user rule so an enrolled directory account must still satisfy the factor it enrolled. `docs/SECURITY.md`, `docs/CONFIGURATION.md`, `docs/SECURITY-LOOSENING.md`, `config/settings.py`, `auth/totp.py` and the `__main__` exposure-gate message all asserted the retired delegation and were corrected; the ADR texts date their claims and were left alone.

**ANCHOR THE RE-READ ON SYMBOLS, NOT LINES -- every line number in this item had drifted before the build started.** `:2060-2061`, `:2112`, `:2130`, `:2378`, `:2412`, `:884` and `:911-914` all named unrelated code at `46ea10a78`. Worse, the item's "both directory legs" is now ONE: the LDAP simple-bind login was retired by BACKLOG #1137 in `97d37bc9a` on 2026-08-22, two days after this research was written, so only the Kerberos leg survived to be changed. A separate trap: `auth/service.py` carries a second, unrelated exemption removal citing BACKLOG #1245 -- that is the bootstrap carve-out on the 6.4.1 temp-password expiry inside `_login_local`, NOT the strength exemption this item names, which was still present in `_mfa_required_for`.

**WHAT STAYS OPEN.** Steps 3 through 6, unchanged and out of the 2026-09-03 scope: (3) federated recentness as one indivisible unit -- a maximum-age parameter on the authorization request, the authentication-time claim validated beside the gate, a refusal when it was requested and the claim is absent or stale, and the session capped accordingly; (4) a federated step-up leg, which needs a session mechanism discriminator; (5) stop seeding the step-up window at login on the directory legs; (6) the minimum-strength fallback statement in the documentation, which is deliberately NOT written -- the doc corrections above describe mechanism and make no claim that this requirement's fallback clause is satisfied.

**THE RESIDUAL, STRENGTHENED rather than corrected, and the correction that was withdrawn stays withdrawn.** Per-function re-checks compare a timestamp the engine stamps itself, and the window is SEEDED AT LOGIN: `_complete_ad_login` defaults `seed_reauth=True`, so the conditional seam never fires for it and the store stamps `reauth_at = now` at session creation. For the whole `step_up_max_age_seconds` window after login (300 default), `has_recent_step_up` is satisfied by the engine's own login stamp with no directory interaction. The build did not touch this -- it is step 5 -- so the residual must record the login-seeded window rather than claim recentness is met. The SSO leg still splits by caller: the console route passes a non-seeding flag under a recorded design note while the JSON negotiate route takes the seeding default, one pathway with two postures depending on which artifact drives it.

**THE DECIDING QUESTION IS STILL UNTAKEN, and no re-score may assume an answer.** Does the 2026-08-16 "an off-by-default control can never be graded pass" ruling reach a CONDITIONAL requirement whose ANTECEDENT -- not merely whose control -- is unreachable on a stock install? Every implementing control here lives inside an identity-provider leg that ships off and cannot ship on without operator configuration (`ad_enabled = true` is refused at settings load without a server and search base, a secure scheme, a bind account and a bind password, all defaulting unset; Kerberos requires AD; federated requires AD plus four more). Under the strict reading this build moves the cell not at all, and the terminal state is a recorded partial. Two cheap moves remain foreclosed by name: arguing that on the shipped default no separate provider exists so the conditional verb is vacuously satisfied, and not-applicable, which fails because the precondition is reachable by configuration.

**WHAT NO BUILD REACHES, and no record may claim otherwise.** Strength and method on the simple-bind and SSO legs, whose protocols return no assertion. The verb routes those to the fallback clause, so any future record claiming they VERIFY strength is wrong on the protocol. The shipped `oidc_require_mfa_claim` default was not changed; the residual names it as a worse-direction re-score trigger.

## 1145. research an honest pass for ASVS 7.1.3 -- federated session-lifetime coordination when the Kerberos leg exposes no ticket lifetime

> 🔢 **Re-scored 2026-08-20 -> P3.** Value **3/10** · Difficulty **5/10** · _fill-in_. Two of three mechanisms ship and the Kerberos limb is untouched: no ticket end time is read anywhere in ldap.py and ADR 0079 still records the cell as accepted-not-built with its promotion trigger unfired. Value is low because the cell carries a signed risk acceptance with a 2027-01-14 review and there is no product effect; difficulty stays mid-band because the honest answer depends on measuring whether any non-SSPI ticket-lifetime source exists on the targeted platforms without forking pyspnego, which the project has ruled out. _(was 3/10 · 5/10.)_
Expand Down
Loading
Loading