SecurityMay 2026 · 1 min read

Hardening a FastAPI service: CSP, rate limits and auth

Most breaches aren't clever. They're a missing header, a default that was never changed, an endpoint with no rate limit. Here's the pass I make on every FastAPI service before it goes near production.

Security headers

Set them once, in middleware, so no route can forget:

@app.middleware("http")
async def security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'"
    return response

For a JSON API, a tight default-src 'none' CSP is fine — you're not serving HTML. Loosen it only where you actually render a page.

Rate limiting

Every auth endpoint needs a limit. Login, OTP, password reset — these are where credential stuffing lives. Back the limiter with Redis so it holds across replicas.

Auth hardening

  • Short-lived access tokens, rotating refresh tokens
  • Hash passwords with a slow algorithm (argon2, bcrypt) — never a raw SHA
  • Constant-time comparison for tokens and signatures
  • Lock the token audience and issuer; validate both on every request

Don't leak in errors

A stack trace in a 500 response tells an attacker your framework versions and file paths. Return a generic error to the client and log the detail server-side.

The point

None of this is exotic. It's a checklist you run every time, so the boring attacks — the ones that actually happen — bounce off.

← All writingGet in touch