🚀
10. Production Security Checklist
++++
Engineering
Apr 2026×15 min read

The concrete security controls in this template and why each one matters — secrets, auth, schema validation, logging hygiene, and automated gates before every deploy.

10. Production Security Checklist

Driptanil Datta
Driptanil DattaSoftware Developer

Security Is a Combination of Controls

No single control makes a service secure. This template's security posture comes from several layers working together: strict schema validation rejecting bad input, middleware enforcing authentication before any handler runs, safe logging that never captures credentials, and automated tooling that runs before code ships. If any one of these is missing, the others compensate less than you'd expect.

This post walks through each control, what it does, and what breaks if you remove it.

1. Override All Default Secrets Before Deploying

The most urgent step. The template ships with default values that work locally but must never reach production:

# server/api/src/services/auth_service.py
self._jwt_secret = os.getenv("AUTH_SECRET", "default_secret")
self._basic_password = os.getenv("AUTH_BASIC_PASSWORD", "admin123")

"default_secret" is in this codebase. Anyone who has read it can sign valid JWTs for your production server. "admin123" is a trivially guessable password. These defaults exist so the app runs out of the box in development — not because they're acceptable anywhere else.

Before deploying, set these in your environment or secret store:

# Validate that required secrets are present and long enough
[ -n "$AUTH_SECRET" ] || { echo "AUTH_SECRET missing"; exit 1; }
[ ${#AUTH_SECRET} -ge 32 ] || { echo "AUTH_SECRET too short"; exit 1; }

Adding this check to your deploy script or CI pipeline makes it impossible to ship without them set. A AUTH_SECRET of at least 32 characters is the minimum; 64 is better.

2. JWT Claims Must Be Fully Enforced

The validate_access_token method uses options={"require": [...]} to make claim enforcement strict:

# server/api/src/services/auth_service.py
payload = jwt.decode(
    token,
    self._jwt_secret,
    algorithms=[self._jwt_algorithm],
    options={"require": ["exp", "id", "name", "email"]},
)

This means a token missing any of exp, id, name, or email raises a PyJWTError and is rejected. More importantly, exp is automatically checked against the current time by PyJWT — tokens that have expired are invalid without any additional code.

The common mistake is disabling expiration checking during development and forgetting to re-enable it:

# What not to do in any environment
options={"verify_exp": False}

With expiration disabled, a token issued once is valid forever, even after you rotate the secret. Any token that was ever valid becomes permanently valid.

3. Schema Validation Is Your First Line of Defense

extra="forbid" on all request models means unknown fields in the request body are rejected before your handler runs:

# server/shared/src/schemas/todo.py
class CreateTodoRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

Without this, a client can send {"title": "test", "__proto__": {}} or {"title": "test", "internal_flag": true} and the extra fields pass through silently. In a simple CRUD API this may not cause direct harm, but it's a class of unexpected input that has caused real vulnerabilities in more complex systems.

StringConstraints and field_validator normalize inputs before they reach the service. Whitespace-only titles fail validation. Descriptions are trimmed to None if they're blank. Your database never stores malformed data because it never reaches the repository.

4. Never Log Authentication Credentials

If you log the Authorization header for debugging, you've written every token and password that passes through your service to your log files, your log aggregator, and anywhere logs are forwarded. This data lives in those systems long after the credentials are rotated.

# What not to do
logger.error("Authorization=%s", request.headers.get("Authorization"))

The safe version logs operational context only — request ID, method, path, status code, timing:

logger.bind(
    event="http.request.completed",
    request_id=request_id,
    method=request.method,
    path=request.url.path,
    status_code=response.status_code,
    duration_ms=round(duration_ms, 3),
).info("")

Also keep LOG_DIAGNOSE=false in production. When enabled, Loguru includes local variable values in tracebacks — and if a token or password happens to be in scope during an exception, it ends up in your logs.

5. Auth Enforcement Is Centralized, Not Per-Route

Authentication is enforced by middleware at the path prefix level. Routes under /api/todos and /api/auth/me are protected regardless of what the route handler does:

PROTECTED_PATH_PREFIXES = ("/api/todos", "/api/auth/me")

This means you can't accidentally ship an unprotected route by forgetting to add an auth check to a new handler. The middleware enforces the rule; individual routes don't need to remember it. If you add a new endpoint under /api/todos/stats, it's protected automatically.

The alternative — checking auth inside each route handler — means one missed check is a security hole.

6. Run All Four Gates Before Every Deploy

Quality tooling only protects you if it runs consistently:

poetry run ruff check .         # catch code quality issues before they ship
poetry run python -m pytest     # verify all tests including auth and rejection paths
poetry run bandit -r server     # find Python security antipatterns statically
poetry run pip-audit            # check your full dependency tree for known CVEs

Each tool covers a different attack surface. ruff and pytest cover correctness. bandit covers implementation mistakes like subprocess.call(shell=True) or hardcoded crypto keys. pip-audit covers supply chain risk from vulnerable dependencies.

None of these run automatically unless you wire them into CI. The safest place to enforce them is as required checks on pull requests — failing a bandit scan blocks the merge. Once they're automated, the decision to skip them requires an explicit override rather than just forgetting to run the command.

Putting It Together

The production security posture of this template isn't one feature — it's the combination of all these controls working in sequence:

  • Input arrives and is rejected if it doesn't match the schema
  • Requests to protected paths are checked by middleware before any handler runs
  • Business logic operates on validated, normalized domain types
  • Responses are mapped through explicit schemas — no internal data leaks
  • Logs capture operational context without credentials
  • Every deploy validates secrets, runs tests, and scans for vulnerabilities

Remove any one layer and you reduce the overall defense without removing the threat. Keep them all, and you have a baseline that's meaningfully harder to compromise than the typical FastAPI starter project.

Drip

Driptanil Datta

Software Developer

Building full-stack systems, one commit at a time. This blog is a centralized learning archive for developers.

Legal Notes
Disclaimer

The content provided on this blog is for educational and informational purposes only. While I strive for accuracy, all information is provided "as is" without any warranties of completeness, reliability, or accuracy. Any action you take upon the information found on this website is strictly at your own risk.

Copyright & IP

Certain technical content, interview questions, and datasets are curated from external educational sources to provide a centralized learning resource. Respect for original authorship is maintained; no copyright infringement is intended. All trademarks, logos, and brand names are the property of their respective owners.

System Operational

© 2026 Driptanil Datta. All rights reserved.