2 Commits
Author SHA1 Message Date
jw bd5c861436 feat(config): make the per-IP rate-limit tiers operator-tunable
The tiers were compile-time constants with a comment noting that operator-tunable
config could follow. This is that, with the constants as defaults, so an
operator who sets nothing sees exactly the previous behaviour.

The burst is what needed a knob, not the rate. One wallet session spends FIVE
strict tokens: /auth/check-restore, /auth/pow-challenge, the two nostr register
calls, and /auth/me. A 20-token bucket therefore holds four onboardings, so four
people unlocking behind one NAT (an office, a household, a VPN exit) exhaust it
and the fifth is throttled with nothing on screen to explain it. Measured
2026-08-24 against production while diagnosing an unrelated flake, where
/auth/check-restore answered 429 RATE_LIMITED.

Raising the burst leaves the sustained rate untouched, and the sustained rate is
the actual abuse control: a flood still converges on period_ms. That is why this
is a burst knob rather than an IP allowlist. An allowlist would remove abuse
protection entirely for the listed address while doing nothing for the shared-NAT
users who are the ones actually hitting this.

Zero is rejected at startup for both period and burst. It reads like
'unlimited' and means the opposite: no token is ever issued and every request is
refused.

NOT DEPLOYED. Production changes go through such-fleet with a --check/--diff
pass; this is the reviewable change, not the rollout.
2026-08-26 13:58:21 -04:00
jw 81a3e4cf58 fix: pre-ship audit remediation for auth identity, abuse control and retention
Fixes every confirmed backend finding from the v0.3.0 pre-ship review.

Identity (the two that corrupt accounts):
- nostr_register creates a row with pubkey_hash NULL, so that wallet's later
  /auth/extension call matched nothing, fell through the rotation path (which no
  client ever satisfies, since nothing sends rotation_signature) and minted a
  SECOND account. A fingerprint match on a NULL-pubkey_hash row now adopts the
  submitted pubkey_hash onto that row, but only when the row's on-file BTC key
  equals the submitted one, so a bare fingerprint (which check_restore discloses,
  and is therefore not a secret) still cannot claim an account.
- The fingerprint-ownership check no longer short-circuits on returning_by_pubkey.
  It was skipped for returning users, so a fingerprint owned by another row got
  backfilled, violated the UNIQUE index, and 500'd that account on every
  subsequent login.
- The rotation proof is verified over a distinct, instance- and target-bound
  message. It shared the login message, so a captured login signature was a
  replayable rotation proof against another instance.

Abuse control:
- The rate limiters key on the real client IP, honouring X-Forwarded-For only
  from a trusted proxy. Behind the TLS terminator every request looked like
  127.0.0.1, so the whole internet shared one bucket and ~3 req/s 429'd every
  wallet unlock globally.
- A PoW solution is single-use instead of replayable for the challenge TTL.
- Delegated /verify is opt-in and rate-limited; it was an unauthenticated
  amplifier into Electrum, LWS and a full-genesis grin scan.
- A grin scan with no start_height gets the same depth policy and PoW price as
  one that passes a height, instead of an unbounded free scan.

Retention and logging:
- The retention sweep is implemented AND scheduled. RETENTION_* was advertised
  and inert, which made the privacy policy's retention promise untrue. Zero
  means keep forever, and each statement is batched so a backlog drains over
  ticks rather than holding row locks.
- login_events.ip_hash is no longer written, and legacy values are scrubbed:
  nothing ever read it, so it was retention without a purpose.
- Tracing spans log user_id only, not the raw npub and username.
- secret-scan.sh matches the coverage its comment claims.

Also fixes the test suite, which did not compile on this commit: NewSession
gained an id in c64cd9b and two call sites were never updated. mint_session now
binds one session_id for both the token and the row, since a different id there
would silently recreate the bug c64cd9b fixed.
2026-08-13 10:31:02 -04:00