CP7 · Runbook · review only — no execution

Enforce AAL2 — with a grace gate that cannot lock anyone out

The final auth-window step. Runs only after cp5 is proven stable. Nothing here has been run.

On this page

  1. What you do from your end — the short list
  2. Current state — why we can't just flip it
  3. The structural guarantee (no lock-out, ever)
  4. The four layers
  5. Rollout — enroll drive org-by-org
  6. Break-glass & recovery
  7. Verification matrix
  8. Rollback
  9. Go / no-go gate

1 · What you do from your end Operator

cp7 is not one flip — it's a paced rollout. Your part:

  1. Approve the enrollment drive first. Before any enforcement, staff must enroll. Right now only 3 of 36 users have an authenticator. Nothing can be enforced until that number climbs.
  2. Send the enroll ask (we can draft the message + link them to /pure-mfa.html). Give a deadline — e.g. 14 days.
  3. Choose the enforcement order — recommend starting with one small org (or just admins) to prove it, then widening.
  4. Give go per org once its people are enrolled. Code flips that org's flag; you test one login with 2FA.
  5. Be reachable for break-glass — as super-admin you can reset a stuck user's factor. (~2 min.)
You will never be asked to "flip MFA on for everyone at once." The guard in §3 makes that impossible by design.

2 · Current state — why we can't just flip it grounded 7/7

36
total users
3
have a verified authenticator
33
have none — would be locked out
aal1
every active session today
If we hard-required AAL2 right now, 33 of 36 people (92%) could not sign in. That single fact is the entire reason cp7 is a grace-gated rollout, not a toggle.

Enrollment plumbing already exists: the pure-mfa.html TOTP card (cp6), and the pure_mfa_enroll + pure_mfa_recovery tables. What's missing is a per-org "required by" date and the gate logic — both additive.

3 · The structural guarantee — no lock-out, ever

The gate follows one rule that makes lock-out impossible:

Never require a second factor from a user who does not have one.

Concretely, at sign-in the gate asks three questions in order:

  1. Does this user's org require MFA yet? If no allow (aal1). Most orgs stay off until you turn them on.
  2. Does this user have a verified factor? If no they are in grace: allowed in, but shown an "enroll now" nudge (hard-blocking to the enroll page only after the org's deadline, and even then the enroll page itself is always reachable).
  3. They have a factor and the org requires it complete the 6-digit challenge to reach aal2. This is the only case that ever demands a code — and only from someone who provably can produce one.

Paired with the pre-flight guard — an org's flag only flips once 100% of its active users have a verified factor — case 2-after-deadline effectively never fires for real staff. A brand-new hire still lands on the always-open enroll page, never a dead end.

4 · The four layers Code

LayerWhat it isType
Per-org flagAdd mfa_required boolean + mfa_required_at timestamptz to pure_orgs. Default off. This is the master switch you control per org.Additive schema
Client gatepure-auth.js reads the aal claim from the access token after login and applies the §3 three-question logic — redirect to challenge or enroll as needed. UX layer.Additive JS
Server RLS (durable)On sensitive tables, add a policy requiring (auth.jwt()->>'aal')='aal2' — but only via a helper that exempts users whose org isn't enforcing yet. The real boundary a bypassed client can't dodge.Additive policy
Break-glass + recoveryRecovery codes (pure_mfa_recovery) issued at enroll; super-admin can reset a user's factor. Guarantees a way back in.Existing + wiring
The client gate is convenience; the RLS layer is the security. We add the client gate first (visible, reversible), and only add aal2 RLS to an org's data once that org is fully enrolled + enforcing.

5 · Rollout — enroll drive, then org-by-org

Phase A — enroll drive Operator

Announce it, link /pure-mfa.html, set a 14-day deadline. Track the count climb from 3/36. No enforcement in this phase.

Phase B — schema + gate (dormant) Code

Add the pure_orgs MFA columns (all off) and ship the pure-auth.js gate reading them. With every flag off, behavior is unchanged — safe to deploy anytime.

Phase C — nudge (soft) Code

Turn on a non-blocking "enroll now" banner for users without a factor. Still no blocking. Measures readiness.

Phase D — enforce one org Op + Code

Pick a small, fully-enrolled org (or the admins). Pre-flight guard confirms 100% enrolled flip its mfa_required=true. You test one login with 2FA. If good, add aal2 RLS to that org's sensitive data.

Phase E — widen

Repeat D org by org as each reaches full enrollment. Never a global flip.

6 · Break-glass & recovery

7 · Verification matrix (per org, at Phase D)

TestExpectedWho
Enrolled user, correct codeReaches aal2, full accessOp
Enrolled user, wrong codeBlocked at challenge, retry — not locked outOp
User in a non-enforcing orgSigns in normally (aal1), unaffectedOp
New hire, no factor, enforcing orgLands on the always-open enroll page — never a dead endCode
Recovery codeGrants entry, prompts re-enrollCode
aal1 session hitting aal2-gated dataRLS denies at DB (durable)Code

8 · Rollback seconds

Per org: set mfa_required=false on that org — enforcement stops instantly for its users. Global: a single window.PURE_MFA_ENFORCE_OFF kill-flag in the client gate disables all challenge redirects; dropping the aal2 RLS policies reverts the durable layer. No data is changed by any of it.

Same philosophy as every prior checkpoint: the enforcement is a flag, not a migration — reversible with one setting, zero residue.

9 · Go / no-go gate

Sequence: cp5 (verify_jwt) stable a day cp7 Phase A–C (safe, dormant) cp7 Phase D org-by-org. cp7 is the last thing, and it moves at the speed of enrollment.