|
| 1 | +# Going to production |
| 2 | + |
| 3 | +The dev defaults are tuned for one process: state lives in memory, the rate limiter is in-process, |
| 4 | +and cookies are configured to work over plain HTTP. None of that survives more than one worker. |
| 5 | +Going to production is four changes, and the security model and the API stay identical, what moves |
| 6 | +is *where state lives* and the operational wiring around it. |
| 7 | + |
| 8 | +This builds on any of the earlier recipes; the auth config itself doesn't change. |
| 9 | + |
| 10 | +## 1. Move state to Redis |
| 11 | + |
| 12 | +CRUDAuth keeps server-side state (sessions, CSRF tokens, lockout counters, single-use email and |
| 13 | +OAuth tokens) in a pluggable store. In memory, that state lives in the process, so under multiple |
| 14 | +workers or pods it isn't shared, which silently weakens lockout counters, sessions, and the |
| 15 | +atomicity of one-time tokens. Point both the session store and the rate limiter at Redis: |
| 16 | + |
| 17 | +```python title="main.py" |
| 18 | +from crudauth import CRUDAuth, SessionTransport |
| 19 | +from crudauth.ratelimit import redis_rate_limiter |
| 20 | + |
| 21 | +REDIS_URL = os.environ["REDIS_URL"] |
| 22 | + |
| 23 | +auth = CRUDAuth( |
| 24 | + session=get_session, user_model=User, SECRET_KEY=os.environ["SECRET_KEY"], |
| 25 | + transports=[SessionTransport(backend="redis", redis_url=REDIS_URL)], |
| 26 | + rate_limiter=redis_rate_limiter(REDIS_URL), |
| 27 | +) |
| 28 | +``` |
| 29 | + |
| 30 | +`SessionTransport(backend="redis")` moves sessions, CSRF, and the one-time-token and OAuth-state |
| 31 | +stores to Redis; `redis_rate_limiter(...)` moves the lockout and throttle counters. CRUDAuth logs a |
| 32 | +startup warning whenever an in-memory backend is active, so a multi-worker deploy left on the |
| 33 | +default won't fail silently; if you've *deliberately* chosen in-memory on a single worker, pass |
| 34 | +`warn_on_memory_backend=False`. |
| 35 | + |
| 36 | +## 2. Wire the lifespan |
| 37 | + |
| 38 | +Redis backends open connections on startup, so call `initialize()` and `shutdown()` from your app's |
| 39 | +lifespan (it's required for Redis and a no-op for in-memory): |
| 40 | + |
| 41 | +```python title="main.py" |
| 42 | +from contextlib import asynccontextmanager |
| 43 | +from fastapi import FastAPI |
| 44 | + |
| 45 | +@asynccontextmanager |
| 46 | +async def lifespan(app): |
| 47 | + await auth.initialize() |
| 48 | + yield |
| 49 | + await auth.shutdown() |
| 50 | + |
| 51 | +app = FastAPI(lifespan=lifespan) |
| 52 | +app.include_router(auth.router) |
| 53 | +``` |
| 54 | + |
| 55 | +## 3. Secrets and cookies |
| 56 | + |
| 57 | +Two things the dev recipes glossed over: |
| 58 | + |
| 59 | +- **The secret comes from the environment**, never a literal. `SECRET_KEY` signs every session and |
| 60 | + token; rotating it invalidates them all, so treat it like any other production secret. |
| 61 | +- **Cookies are secure by default.** `CookieConfig.secure` is `True` unless you turned it off, so |
| 62 | + serve over HTTPS and the session cookie is sent only over TLS. The dev recipes used |
| 63 | + `CookieConfig(secure=False)` to work on `http://localhost`; drop that in production. Don't ship |
| 64 | + `secure=False`. |
| 65 | + |
| 66 | +## 4. Behind a load balancer |
| 67 | + |
| 68 | +If you run behind a reverse proxy or load balancer, the socket peer is the proxy, not the user, so |
| 69 | +IP-based throttles would see every request as one client. Tell CRUDAuth how many trusted proxies sit |
| 70 | +in front so it reads the real client IP from `X-Forwarded-For`: |
| 71 | + |
| 72 | +```python |
| 73 | +auth = CRUDAuth(..., trusted_proxy_hops=1) # one proxy/LB in front |
| 74 | +``` |
| 75 | + |
| 76 | +Set it to the actual number of hops you control; the default `0` ignores the header and uses the |
| 77 | +socket peer (correct when nothing is in front, safe when you're unsure). |
| 78 | + |
| 79 | +## What production actually changes |
| 80 | + |
| 81 | +Notice what didn't change: not the routes, not the gates, not a line of your authorization code. The |
| 82 | +API is identical to development. What changed is that state is now *shared and durable* (Redis), so |
| 83 | +lockout counters, sessions, and one-time-token atomicity actually hold across every worker and |
| 84 | +across restarts, and the operational edges are wired (lifespan connections, secrets from the |
| 85 | +environment, TLS-only cookies, the real client IP). CRUDAuth makes the one dangerous default loud |
| 86 | +rather than silent: it warns when a memory backend is active, so the multi-worker footgun announces |
| 87 | +itself instead of quietly degrading your security. |
| 88 | + |
| 89 | +## Where to go next |
| 90 | + |
| 91 | +- The backends in depth: [Storage & lifespan](../guides/infra/storage.md). |
| 92 | +- Tuning the limits: [Rate limiting & lockout](../guides/infra/rate-limiting.md). |
| 93 | +- Back to the recipes: the [cookbook index](index.md). |
0 commit comments