82 lines
3.4 KiB
Markdown
82 lines
3.4 KiB
Markdown
|
|
---
|
||
|
|
kind: task
|
||
|
|
slug: rate-limit-and-abuse-guardrails
|
||
|
|
title: Rate-limit credentials sign-in and high-impact mutations
|
||
|
|
plan_slug: multitenant-saas-hardening
|
||
|
|
epic_slug: tenant-lifecycle
|
||
|
|
status: ready
|
||
|
|
priority: P2
|
||
|
|
tenant_id: global
|
||
|
|
owner: unassigned
|
||
|
|
cursor_todo_id: null
|
||
|
|
updated_at: "2026-06-01"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Task summary
|
||
|
|
|
||
|
|
Add per-IP rate limits to credentials sign-in and to a small set of high-impact mutation endpoints (invite, archive, create-workspace). The goal is to cap brute-force and obvious abuse, not to be a full WAF.
|
||
|
|
|
||
|
|
## Description
|
||
|
|
|
||
|
|
### Storage
|
||
|
|
|
||
|
|
Use the existing Redis instance on CT 102 (already used by Hocuspocus for awareness). Add a thin client in `packages/shared/src/rate-limit/` — token-bucket or sliding-window, your choice; the contract is `consume(key: string, opts: { capacity, refillPerSecond }): Promise<{ ok: boolean, remaining: number, retryAfterMs: number }>`.
|
||
|
|
|
||
|
|
If you don't want a new shared lib, `@upstash/ratelimit` is a fine drop-in even though we're not on Upstash — it works against any Redis URL. Don't add a new database for this.
|
||
|
|
|
||
|
|
### Where to apply
|
||
|
|
|
||
|
|
1. **Credentials sign-in** (`apps/web/app/api/auth/[...nextauth]/route.ts`, or the `authorize` callback): key by IP (`x-forwarded-for`, falling back to remote addr). 5 attempts / 60 seconds, then 429.
|
||
|
|
2. **Invite create**: key by `actor_user_id`. 30 invites / hour. Workspace-scoped is fine too.
|
||
|
|
3. **Workspace create**: key by `actor_user_id`. 5 workspaces / hour.
|
||
|
|
4. **Archive/restore**: key by `(workspace_id, actor_user_id)`. 20 / hour. Cheap insurance against a script flipping state in a loop.
|
||
|
|
|
||
|
|
### IP extraction
|
||
|
|
|
||
|
|
Use the same helper everywhere; don't recompute in every route. Common gotcha: `x-forwarded-for` is a comma-separated list when there are multiple proxies. Take the first entry. If the deployed stack is behind Cloudflare, `cf-connecting-ip` is more reliable — add that as a higher-priority source if present.
|
||
|
|
|
||
|
|
### Response
|
||
|
|
|
||
|
|
When a limit trips, return `429 Too Many Requests` with a `Retry-After` header in seconds and a tRPC `TOO_MANY_REQUESTS` error code. The client should surface a friendly message ("try again in ~30 seconds"), not a stack trace.
|
||
|
|
|
||
|
|
### Audit logging
|
||
|
|
|
||
|
|
Every limit trip writes an `audit_log` row (`action: "rate_limit.tripped"`, `metadata: { route, key_kind, retry_after_ms }`). This is how you discover whether anyone's hitting the limits in practice; without it the limits are silent.
|
||
|
|
|
||
|
|
### Anti-goals
|
||
|
|
|
||
|
|
- Don't add a CAPTCHA. If brute-forcing becomes a real problem, add Cloudflare in front of the deploy.
|
||
|
|
- Don't write a generic rate-limit middleware that wraps every tRPC procedure. The set of high-value endpoints is short; explicit is better than universal.
|
||
|
|
|
||
|
|
## Subtasks
|
||
|
|
|
||
|
|
- [ ] Pick the Redis client and rate-limit lib (or hand-roll).
|
||
|
|
- [ ] Add IP extraction helper.
|
||
|
|
- [ ] Apply limits to the 4 routes above.
|
||
|
|
- [ ] Wire `audit_log` writes on limit trips.
|
||
|
|
- [ ] Verify by scripting 20 credentials sign-in attempts against a dev deploy.
|
||
|
|
|
||
|
|
## Owner or assignee
|
||
|
|
|
||
|
|
Unassigned
|
||
|
|
|
||
|
|
## Status
|
||
|
|
|
||
|
|
ready
|
||
|
|
|
||
|
|
## Estimation
|
||
|
|
|
||
|
|
M
|
||
|
|
|
||
|
|
## Acceptance criteria
|
||
|
|
|
||
|
|
- [ ] Brute-forcing credentials sign-in trips at attempt 6 within 60 seconds.
|
||
|
|
- [ ] Invite-spam attempt trips at invite 31 within an hour.
|
||
|
|
- [ ] Every trip produces an `audit_log` row.
|
||
|
|
- [ ] No global tRPC middleware — limits are applied per route.
|
||
|
|
|
||
|
|
## Links to related Epic / Plan
|
||
|
|
|
||
|
|
- Epic: `./Epic-tenant-lifecycle.md`
|
||
|
|
- Plan: `../Plan-multitenant-saas-hardening.md`
|