Skip to content

Add global rate limit and /.well-known/security.txt#24

Merged
amrtgaber merged 1 commit into
mainfrom
feat/rate-limit-and-security-txt
May 13, 2026
Merged

Add global rate limit and /.well-known/security.txt#24
amrtgaber merged 1 commit into
mainfrom
feat/rate-limit-and-security-txt

Conversation

@amrtgaber
Copy link
Copy Markdown
Contributor

Production-hardening additions ported from vagrant-story-api's launch prep — closes #22 and #23.

1. Global default rate limit (#22)

  • Limiter now has default_limits=["60/minute"] and SlowAPIMiddleware is installed — without the middleware, only routes with an explicit @limiter.limit(...) are limited; with it, the default applies to every route. (We don't have any @limiter.limit-decorated routes; the auth limits are done via a path-matching middleware because the fastapi-users routers are generated.)
  • The existing per-path auth limits (5/min login, 3/min register, 30/min refresh) stack on top of the global default — an auth route is subject to both. The stricter auth limit is the binding constraint in practice; the global one is a backstop.
  • Infrastructure routes — /, /health, /docs, /.well-known/security.txt — are @limiter.exempt so health-check probes, OpenAPI/doc fetches, and automated scanners can't burn through the quota. (/openapi.json and /redoc are FastAPI built-ins and aren't easily decoratable; 60/min is plenty for spec fetches and codegen.)
  • Limits are keyed by request.client.host, which behind a proxy/load balancer is the proxy's IP unless uvicorn trusts X-Forwarded-For — see chore: add info about open api #3 below.

2. /.well-known/security.txt (#23)

  • New unversioned route serving text/plain per securitytxt.org, with a placeholder Contact: mailto:security@example.com and Expires: 2027-05-12 (one year out).
  • The README's "before first deploy" note now lists replacing the contact (alongside the existing COOKIE_PREFIX reminder).

3. Proxy-header trust in start.sh

  • start.sh now runs uvicorn ... --proxy-headers --forwarded-allow-ips='*' so request.client.host resolves to the real client IP behind a proxy (Cloud Run, ALB, nginx). Without it, the IP-keyed rate limiter collapses every request into one global bucket. A comment in start.sh explains the trust assumption (* is correct only when the service is always behind a proxy that overwrites the header).

Tests

tests/test_app.py:

  • security.txt is served as text/plain with Contact: and Expires:.
  • The 60/min default 429s an undecorated route (/v1/notes) on the 61st call within the window.
  • Infrastructure routes (/health, /, /.well-known/security.txt) survive 70+ rapid hits without a 429.

conftest.py gets an autouse reset_rate_limiter fixture (clears limiter._storage before/after each test) so request counts don't leak between tests.

Verification

uv run pytest — 32 passed. uv run ruff check . / ruff format --check . clean. sh -n start.sh clean. App boots; route list confirmed (/.well-known/security.txt present and unversioned; infra routes exempt).

…trust

Production-hardening additions ported from vagrant-story-api's launch prep:

- Global default rate limit (60/min per client IP) via SlowAPIMiddleware +
  `default_limits` on the Limiter. Applies to every route that isn't
  decorated with its own limit or marked exempt; the stricter per-path auth
  limits stack on top. Infrastructure routes (`/`, `/health`, `/docs`,
  `/.well-known/security.txt`) are `@limiter.exempt` so health probes, spec
  fetches, and automated scanners can't burn through the quota.
- `/.well-known/security.txt` per securitytxt.org, with a placeholder
  Contact and a 1-year Expires; the README "before first deploy" note now
  flags both for replacement.
- `start.sh` runs uvicorn with `--proxy-headers --forwarded-allow-ips='*'`
  so `request.client.host` resolves to the real client IP behind a
  proxy/load balancer — without it the IP-keyed rate limiter collapses
  every request into one bucket. A comment spells out the trust assumption.

Tests (tests/test_app.py): security.txt content; the default limit 429s an
undecorated route on the 61st call within the window; infra routes don't.
conftest resets the limiter storage between tests so counts don't leak.

Closes #22, closes #23.
@amrtgaber amrtgaber merged commit 8a1f592 into main May 13, 2026
2 checks passed
@amrtgaber amrtgaber deleted the feat/rate-limit-and-security-txt branch May 13, 2026 04:33
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.

Add a global default rate limit

1 participant