Admin: per-account rate-limit hit history#136
Merged
Conversation
The aggregate Prometheus counters answer "is something being
hammered"; admins triaging a specific account had nothing answering
"what has THIS account tripped recently". Blocked rate-limit checks
attributable to an authenticated user are now recorded to a capped
per-user Redis list (sheaf:rlh:{user_id}) and exposed via
GET /v1/admin/users/{id}/rate-limit-history plus a section in the
Explain-account panel (per-bucket totals + recent entries, hidden
when clean).
Bounded twice: RATE_LIMIT_HISTORY_MAX_ENTRIES (200) per user and a
RATE_LIMIT_HISTORY_HOURS (48) retention TTL, with
RATE_LIMIT_HISTORY_ENABLED as the kill switch. Only blocked checks
record, so volume is self-limiting (one entry per 429); the write is
best-effort so a Redis blip can never turn a 429 into a 500; nothing
touches Postgres. The read side filters stale entries by timestamp
since a fresh hit refreshes the whole key's TTL.
Privacy: the history is held personal data (IP + behaviour), so it is
included in the admin DSAR dossier and purged on account deletion
rather than left to age out. Anonymous traffic (logged-out login
attempts, the global per-IP backstop) is not attributable to an
account and is never recorded; the metrics cover it in aggregate.
Reading the history writes no audit row, matching the other read-only
triage endpoints (explain, session list).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes out the admin batch 2 backlog. The Prometheus counters answer "is the instance being hammered"; this adds the per-account drill-down for abuse triage: "what has THIS account specifically tripped recently".
How it works
When a rate-limit check blocks a request that carries a resolved authenticated user, the limiter records
{time, bucket, scope, route, ip}to a per-user Redis list (sheaf:rlh:{user_id}). Admins read it viaGET /v1/admin/users/{id}/rate-limit-history(per-bucket summary + entries, newest first) and a new section in the Explain-account panel, which stays hidden when there is nothing to triage.Bounds and failure modes
Storage is bounded twice:
RATE_LIMIT_HISTORY_MAX_ENTRIES(default 200) per user via LTRIM, andRATE_LIMIT_HISTORY_HOURS(default 48) as a retention TTL, withRATE_LIMIT_HISTORY_ENABLEDas the kill switch. Only blocked checks are recorded, so write volume is self-limiting at one entry per 429. The write is best-effort (a Redis hiccup can never turn a clean 429 into a 500), the read endpoint degrades to 503 if Redis is unreachable, and nothing touches Postgres. The read side filters entries older than the retention window by timestamp rather than trusting the key TTL, since a fresh hit refreshes the whole key.Privacy posture
This is held personal data (IP + behaviour), so it follows the same rules as the rest: included in the admin DSAR dossier bundle, and purged outright on account deletion instead of waiting out the TTL. Anonymous traffic (failed logins from logged-out clients, the global per-IP backstop) is not attributable to an account and is never recorded; the aggregate metrics cover those. Reading the history writes no audit row, matching the existing read-only triage endpoints (explain, session list); documented in SELFHOSTING.md.
Tests
Four endpoint tests in the normal suite (seeded Redis round-trip incl. stale-entry filtering, empty history, 404, no-audit-row) plus an end-to-end test in the rate-limit config: an authenticated user trips the 5/60 per-user limit and the admin endpoint returns exactly one entry per 429 with the right scope/route/ip. Full multi-config run: all 9 configurations passed (1064 in the main config).