Stack: DRF + Redis · Project: TicketFlow Status: Published — closing the security module Prerequisite: Lesson 22 — OWASP
Objectives
- Manage secrets without them ending up in Git, logs or Docker images (environment hierarchy and rotation).
- Implement real rate limiting (token bucket with Redis) on login and checkout.
- Apply GDPR to TicketFlow: legal basis, minimization, rights (export/deletion) and audit logs.
1. Secrets: the life cycle
- Never in Git (05):
.envout,env.examplein. A leaked history is repaired by rotating, not deleting. - Environment hierarchy: dev (dummy values), staging (test secrets), prod (KMS/vault: AWS Secrets Manager, HashiCorp Vault, or at minimum: the orchestrator's environment variables — never baked into the Docker image: the image leaks).
- Rotation: every credential has an expiry date in your inventory (Django's
SECRET_KEY: rotate with a session migration plan; gateway keys: dual rotation — publish the new one, migrate, revoke the old one). Successful rotation is the kind you can do without downtime: design for that. - People's access: nobody keeps prod secrets on their machine; temporary, audited access (25/56).
git-secrets/trufflehogin CI to prevent secret commits.
2. Rate limiting: token bucket with Redis
The limit protects: login (brute force, 20), checkout (bots buying everything — TicketFlow's I4-adjacent case), the public API (abuse). Token bucket algorithm: the user has N tokens regenerating at rate R; every request spends one; without a token → 429 with Retry-After.
def allow(key: str, capacity: int, refill_per_min: int) -> bool:
now = time.time()
pipe = redis_pipeline()
data = pipe.hgetall(key).execute() # tokens, last_refill
tokens = min(capacity, float(data.get(b"tokens", capacity)))
elapsed = now - float(data.get(b"last", now))
tokens = min(capacity, tokens + elapsed * refill_per_min / 60)
if tokens < 1:
pipe.hset(key, mapping={"tokens": tokens, "last": now}); pipe.expire(key, 3600)
return False
pipe.hset(key, mapping={"tokens": tokens - 1, "last": now}); pipe.expire(key, 3600)
return TrueProduction: make it atomic with a Lua script (avoids the race between HGETALL and HSET — 10/12 applied to Redis) or use the django-ratelimit library. Keys: by IP for anonymous users, by user for authenticated ones, by sensitive endpoint (login, checkout) with harder limits. And an honest response: 429 + Retry-After + a log of the event (46): a blocked bot is information, not noise.
3. GDPR for the developer (what touches your code)
- Legal basis and minimization: only data needed to sell tickets: name, email, and billing data. The "date of birth just in case" is NOT collected. Less data = less liability.
- Purpose and retention: reservations are retained (fiscal), abandoned carts are NOT: a purge job for old expired (anonymous) PENDING ones — retention is a data decision, documented.
- Right of access/export (Art. 15): a user export endpoint (their profile + reservations + payments, JSON or CSV) — authenticated, their own data, 14 guarantees the export's idempotency.
- Right to be forgotten (Art. 17): delete/no: sales carry a fiscal obligation → anonymization (email → irreversible hash, name → "deleted"), keep the accounting record. Never DELETE payments (audit, 08).
- Consent: marketing separate from operations (a non-mandatory checkbox), revocable in one click.
- Processors: the gateway and the email provider are data processors: a signed DPA (the business's part; your part: knowing what data travels to each one).
4. Audit logs (the stitch that ties it together)
The audit log records WHO did WHAT WHEN on which resource, immutable: role changes (21), refunds, event changes, user-data access. An append-only table (no UPDATE/DELETE by design) + defined retention. It is what turns "we believe nobody touched the payment" into "here is the record" (56 formalizes it for SOC2).
Self-assessment
- Your repo leaked a gateway secret a month ago: what is the only real repair, and how is it done without downtime?
- Why does the token bucket need atomicity (Lua), and what bug does the version with separate HGET/HSET produce?
- Why is the login rate limit per user+IP and not just per IP? Which attack does the per-user limit prevent?
- A user asks to be deleted: why not DELETE their payments, and what exactly is done with their data?
- Which three pieces of data must TicketFlow NOT ask for, and why (minimization)?
Continue with the exercises. The solutions only after trying it yourself.