REVIEW-001 (M0) L5s: a code valid for ±90s could be replayed. TOTP is a
login fallback (D-005) and the only factor the bootstrap admin has (D-041),
so a code observed once (shoulder-surfed, phished, in a log) must not work
twice.
Track the highest accepted counter per identity and refuse any counter that
is not strictly newer. The search is newest-first and compares in constant
time; a replay is reported as ErrInvalidCode — indistinguishable from a wrong
code, so the response is not an oracle (A3). A fresh secret (re-enrollment)
resets the counter.
REVIEW-001 (M0) perimeter findings:
- H1: resolveForwarded walked X-Forwarded-For left→right and returned the
leftmost untrusted entry. An append-style proxy (nginx's default) appends
the peer it saw to the RIGHT of whatever the client sent, so the leftmost
entry is attacker-controlled — a fresh rate-limit bucket per request (A3)
and a forged client_ip in the audit trail (A11/D-031). Walk right→left and
return the first untrusted entry, falling back to the peer.
- L1s: the session cookie now carries the `__Host-` prefix when it is Secure
(https), so it is pinned to the exact origin and cannot be shadowed from a
sibling subdomain. Conditional on purpose: a browser rejects a `__Host-`
cookie without Secure, which would break the http loopback bootstrap.
- L4s: add Permissions-Policy, Cross-Origin-Opener-Policy and
Cross-Origin-Resource-Policy.
- Add httpx.IsLoopbackIP: a RESOLVED client IP test that fails closed on an
empty/unparseable value (the setup-mode gate, H2), sharing a classifier
with IsLoopbackHost whose "unknown" direction is the opposite, each
fail-closed for its own decision.
Tests updated: the unit test encoding the vulnerable leftmost expectation is
rewritten, plus right→left/forged-entry/host-classification coverage.
- One base layout (layout.html) with the header, the nav from Registry.Nav()
(D-038), a content slot and the CSRF token; every page renders inside it and
a module supplies content only
- Embedded same-origin assets (app.css · app.js · webauthn.js · sw.js ·
manifest.webmanifest · icons) served by the reserved `static` workflow
- webauthn.js drives the three ceremonies from data-* attributes — no inline
script, so the CSP `script-src 'self'` holds (AC-3)
- manifest start_url "/" (D-011); the service worker never caches an
authenticated response or /{GUID} (A5)
- Build-time discovery (D-028, the Caddy pattern): a module self-registers from
init(); internal/modules/imports_gen.go (go:generate) blank-imports every
module directory, so THE DIRECTORY IS THE SOURCE OF TRUTH
- The registry: deterministic registration order, config-backed enable/disable
(D-038), required modules, name-prefixed migrations, Mount() into the door
- The name-collision guard now also fails a module named for a reserved
level-1 workflow (SPEC-009 §13 Q-1) — access · static · api · media · ws ·
dev — derived from routing.ReservedWorkflows, `home` excepted (D-2)
- home: the reference dashboard module, and the core Modules page
Reserved, setup-mode-only, loopback-only: the domain, the top admin, the
authenticator (TOTP), mail/locale — then Finish persists and the dashboard
becomes reachable. The wizard IS the bootstrap (D-026); the TOTP key is
generated at startup so TOTP enrollment precedes Finish (SPEC-009 D-1).
- webauthn: the passkey ceremony service (a fake in tests)
- totp: the TOTP authenticator; the secret is AEAD-sealed at rest (A9)
- sealer: the AEAD TOTP sealer
- identities/sessions/credentials: the GUID state machine
- flow: the enroll/login screens + the passkey nudge (pre-session
double-submit CSRF — A15), rendered inside the shell
- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded
migration runner; each module's migrations are name-prefixed (D-012)
- audit: the append-only audit log; the actor threads via context (R-1)
- users: the user_id/GUID/email anchors (D-013)
- config: the YAML settings file (D-035) + validation
- httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security
headers · redaction
- middleware: correlation id · audit · rate limiting
- mail: the invite/recovery mailer seam
- server: the core DOOR — the resolution switch, the dispatch registry
(RegisterModule), the role gates, /setup · enroll/login · recovery flows,
the reserved `static` workflow, and the loopback-only bootstrap (D-025)
Turns a request path plus the two routing fields (`do`/`action`) into a single
Resolution for BOTH doors (D-039): the RESERVED level-1 switch first
(home/access/setup/api/static/media/ws/dev — D-004), then the GUID classifier,
then the registry. Identity and the module registry are injected interfaces, so
the package is PURE — no HTTP stack, no DB (asserted by purity_test.go). All 22
spec rows are covered.