Skip to content

docs: document the opt-in account lockout (NIST SP 800-63B §5.2.2) - #8

Merged
stsepelin merged 3 commits into
mainfrom
docs/account-lockout
Aug 21, 2026
Merged

stsepelin merged 3 commits into
mainfrom
docs/account-lockout

Conversation

@stsepelin

@stsepelin stsepelin commented Aug 20, 2026 •

Copy link
Copy Markdown
Owner

Adds the missing docs for features.lockout, shipped in lukk #22. Nothing about it was documented — not the flag, the settings, the migration group, lukk:release, the 423, the events, or the repository seam.

New page — /account-lockout (Features)

Leads with the distinction that's easy to conflate, since the docs already used the word "lockout" loosely for the 429 throttle:

Rate limit (always on) Lockout (opt-in)
Bounds a rate a run of consecutive failures
Storage cache, decaying lukk_lockouts, persistent
Cleared by time, or a success a success only
Status 429 423 Locked

Then: setup + the publish group, max_attempts/release_after with the trade-off stated rather than buried (a hard lockout is a DoS primitive — which is why it's off by default), the login/two_factor purposes, the recovery-code exemption, all four release paths, lukk:release, pruning, both events, and the LockoutRepository swap seam with its atomicity warning.

It recommends release_after=3600: that gives up the strict §5.2.2 reading, but lands exactly on OWASP ASVS V2.2.1 — a bar the package doesn't otherwise clear — and the lock lifts itself.

Cross-links

configuration (rate limits + feature toggles), authentication, two-factor, installation, events, security (standards mapping + checklist), customization, deployment.

Two pre-existing gaps fixed

The new content reads as wrong sitting next to these:

  • the features toggle list was missing email_verification, password_reset, registration
  • the login rate limit documented ip_max_attempts but never account_max_attempts — which is the cap the lockout is actually measured against (20/60s ≈ 1,200 failures an hour, indefinitely)

Notes

Greptile Summary

This PR adds comprehensive documentation for the opt-in persistent account-lockout feature and connects it throughout the existing authentication, configuration, operations, events, customization, and security guidance.

  • Adds the Account Lockout feature page and sidebar entry.
  • Documents setup, configuration, status codes, release paths, pruning, events, and storage customization.
  • Adds related configuration keys, feature toggles, cross-links, and standards guidance across existing pages.

Confidence Score: 4/5

The PR appears safe to merge, with two non-blocking documentation consistency issues in the configuration description and customization cross-reference.

The new feature documentation is internally coherent overall, but one sentence no longer matches the expanded login configuration shape and the advertised full contract list omits the new lockout repository seam.

Files Needing Attention: docs/configuration.md, docs/account-lockout.md, docs/customization.md

Important Files Changed

Filename Overview
docs/account-lockout.md Adds the complete lockout guide; its final cross-reference points to a contract list that omits the newly documented repository seam.
docs/configuration.md Adds lockout and account-throttle configuration, but leaves the introductory rate-limit shape inconsistent with the expanded example.
docs/security.md Adds the optional consecutive-failure control to the standards map and deployment checklist.
docs/events.md Documents the new account lock and release events, their payloads, and intended listener usage.
docs/deployment.md Extends pruning guidance to include spent lockout counters.
docs/two-factor-authentication.md Explains TOTP lockout behavior and the recovery-code exemption.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Authentication attempt] --> B{Lockout enabled?}
  B -->|No| C[Apply rate limits]
  B -->|Yes| D{Existing lock held?}
  D -->|Yes| E[Return 423 Locked]
  D -->|No| C
  C --> F{Authentication succeeds?}
  F -->|Yes| G[Release counter]
  F -->|No| H[Record consecutive failure]
  H --> I{Failure cap reached?}
  I -->|No| J[Return authentication error]
  I -->|Yes| K[Persist lock and dispatch AccountLocked]
Loading

Comments Outside Diff (1)

  1. docs/configuration.md, line 62 (link)

    P2 Rate-limit shape is inconsistent

    The introduction says login adds a third key, but the changed example now adds both ip_max_attempts and account_max_attempts, leaving the prose inconsistent with the canonical configuration shape.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: docs/configuration.md
    Line: 62
    
    Comment:
    **Rate-limit shape is inconsistent**
    
    The introduction says login adds a third key, but the changed example now adds both `ip_max_attempts` and `account_max_attempts`, leaving the prose inconsistent with the canonical configuration shape.
    
    
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix all with Greploop Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
docs/configuration.md:62
**Rate-limit shape is inconsistent**

The introduction says login adds a third key, but the changed example now adds both `ip_max_attempts` and `account_max_attempts`, leaving the prose inconsistent with the canonical configuration shape.

```suggestion
Every throttle lives here, each shaped as `{ max_attempts, decay_seconds }` (login adds two additional keys, `ip_max_attempts` and `account_max_attempts`):
```

### Issue 2
docs/account-lockout.md:169
**Contract cross-reference is incomplete**

This link advertises Customization as the full list of rebindable contracts, but that list omits the newly documented `LockoutRepository` seam, so readers following the reference cannot find or verify it.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "docs: document the opt-in account lockou..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Comment thread docs/account-lockout.md
lukk 0.5.0 ships `features.lockout` and none of it was documented — the flag,
`max_attempts`/`release_after`, the `lukk-lockout-migrations` group,
`lukk:release`, the 423, `AccountLocked`/`AccountReleased`, or the
`LockoutRepository` seam.

The page leads with the distinction that actually matters, because the two are
easy to conflate: the existing throttles bound a RATE and decay, the lockout
bounds a RUN and persists. It also states the trade-off rather than burying it
— a hard lockout is a denial-of-service primitive, which is why it is off by
default — and recommends `release_after=3600`, which gives up the strict
§5.2.2 reading but lands exactly on OWASP ASVS V2.2.1 and lifts itself.

Two pre-existing gaps fixed along the way, since the new content reads as
wrong next to them:

- the `features` toggle list was missing `email_verification`,
  `password_reset` and `registration`
- the login rate limit documented `ip_max_attempts` but never
  `account_max_attempts`, which is the cap the lockout is actually measured
  against (20/60s ≈ 1,200 failures an hour, indefinitely)
Follows lukk#23. Three things the lockout page now gets right:

- `confirm` joins `login` and `two_factor` as a lockout purpose, with the
  reasoning for why step-up counts at all (it re-verifies the same password) and
  why `confirm-passkey` is throttled but never locked.
- The `login` subject is identity, not the submitted string — and the `idn:`
  fallback is explained as load-bearing rather than a leftover, since an
  identifier naming no account must still count or 423-vs-422 becomes an
  existence oracle.
- A successful login and a password reset both release a confirm lock.

Also adds a Throttling section to the confirmation page, the `confirm` row to
the rate-limits table, a security-checklist line, and the lockout + throttle
identity entries the roadmap's Shipped list was missing.
Two accepted trade-offs now live where a user evaluating lukk will actually
find them, rather than in a repo-internal register.

**The grace window is a deviation from RFC 9700 §4.14.2 / OAuth 2.1**, and the
rotation page now says so in those words. Both specs describe rotation as
invalidate-and-detect — a replayed invalidated token "will revoke the active
refresh token" — and neither provides for a tolerance window. Within
`grace_seconds` lukk detects the replay and deliberately does not revoke.

Previously this was written as a trade-off the spec accommodates. It isn't, and
saying so is the honest version. What bounds it: strict invalidation logs out
any client with two tabs open (a direct client can't be forced to single-flight);
only the immediately-previous token is tolerated, since grace is measured from
each token's own `rotated_at`; and Okta ships the same 30s default, with Auth0
and fosite doing the equivalent.

**Look-alike identifiers share a rate-limit bucket** — the second limitation.
Worth stating that it does NOT affect the account lockout, which keys on the
resolved user id and so meets NIST SP 800-63B §5.2.2's "single account" scoping
literally; the decaying throttle sits underneath that clause rather than
implementing it.

Also documents what 0.5.0 added and the site never described: the
RefreshFamilyForked event (and why it's advisory — revoking on a fork means
revoking on suspicion), RefreshTokenReused no longer firing for ordinary
post-logout retries, per-guard refresh cookies and TTLs under cookie_mode, the
409 on re-enrolling confirmed two-factor, the reserve-before-verify lockout
semantics, and the production guard against an array cache store.
@stsepelin
stsepelin force-pushed the docs/account-lockout branch from 539a54a to cac68df Compare August 21, 2026 12:40
@stsepelin
stsepelin merged commit 2a84b39 into main Aug 21, 2026
2 checks passed
@stsepelin
stsepelin deleted the docs/account-lockout branch August 21, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant