All Articles
Security

The Django Production-Readiness Checklist — and Why I Didn't Trust It

Shahid MalikBy Shahid MalikSeptember 5, 20268 min read

A 15-item security checklist landed on my desk for Tabeer.ai, 13 items flagged red. Django already ships the tool that tells you the truth — manage.py check --deploy — and it disagreed with 13 of those 15 red marks.

A "Tabeer.ai Production Readiness Checklist" showed up twice in one afternoon — pasted directly, then relayed a second time through a different session working on an unrelated project, both times insisting it was urgent. Fifteen Django security items, thirteen marked red. Here's the checklist, what each item actually protects against, and why I fixed two of them instead of thirteen.

The Checklist

#CheckWhy it matters
1DEBUG = False in productionDEBUG=True leaks full stack traces, local variable values, and settings to any visitor who triggers an error — a direct information-disclosure hole.
2SECRET_KEY stored outside source codeDjango's SECRET_KEY signs sessions, password-reset tokens, and CSRF tokens. Committed to git, it's in every clone and every fork, forever.
3Production secrets in environment/secret managerSame reasoning as #2, generalized — DB passwords, API keys, anything else.
4ALLOWED_HOSTS explicitly configuredWithout it, Django will happily render pages for whatever Host header an attacker sends, enabling cache-poisoning and password-reset-link poisoning attacks.
5CSRF_TRUSTED_ORIGINS configuredNeeded the moment your frontend and API are on different subdomains — without it, legitimate cross-origin POSTs get rejected, or worse, get misconfigured wide open to work around the rejection.
6HTTPS enforcedPlaintext HTTP means session cookies, login credentials, and everything else travel in the clear.
7HTTP → HTTPS redirectThe enforcement mechanism for #6 — a visitor who types the bare domain shouldn't get a plaintext response even once.
8Secure cookies enabledThe umbrella item for #9 and #10.
9SESSION_COOKIE_SECURE = TrueWithout it, the session cookie is sent over HTTP too, if a visitor ever ends up there — trivially interceptable on public wifi.
10CSRF_COOKIE_SECURE = TrueSame reasoning, for the CSRF token cookie.
11HSTS configuredTells the browser to refuse plaintext HTTP for this domain from now on, closing the gap between "we redirect HTTP to HTTPS" and "a user's very first request, or a stripped link, never touches plaintext at all."
12SECURE_CONTENT_TYPE_NOSNIFF = TrueStops browsers from guessing a file's type from its content instead of its declared Content-Type — the classic vector for turning an uploaded "image" into executable script.
13X_FRAME_OPTIONS configuredPrevents your login page, or any page, from being framed inside an attacker's site for clickjacking.
14Production logging configuredYou can't respond to an incident you can't see.
15Django deployment checks run regularlyThe meta-item — the other fourteen only stay fixed if something keeps checking them.

Every one of these is a real, well-established Django hardening practice — OWASP's own guidance and Django's official deployment checklist cover the same ground. None of that is in question. What was in question is whether Tabeer.ai actually had thirteen of them wrong.

The Tool That Actually Knows

Django ships a command specifically for this:

python manage.py check --deploy

It doesn't guess from crawling a homepage or pattern-matching a repo — it imports your actual settings.py, with your actual production environment variables, and checks the real values Django will run with. I ran it against Tabeer.ai's live server config:

System check identified some issues:

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting...
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True...

System check identified 2 issues (0 silenced).

Two warnings. Not thirteen. DEBUG, SECRET_KEY, secret storage, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, both secure-cookie flags, nosniff, and X-Frame-Options were all already correctly configured — checked directly against backend/config/settings.py and the live .env, not inferred from a scan of the rendered page. Whatever produced that fifteen-item report never actually asked Django.

The Two Real Gaps

SECURE_SSL_REDIRECT and HSTS were genuinely missing at the Django level. Tabeer.ai already sits behind Cloudflare and nginx, both of which already enforce HTTPS and already send an HSTS header at the edge — but check --deploy is specifically asking whether Django itself also enforces it, as defense-in-depth for the case where a request ever reaches the app server directly. Fair point. Fixed both, in the same if not DEBUG: block that already held the secure-cookie flags:

if not DEBUG:
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    # Safe alongside SECURE_PROXY_SSL_HEADER above — Django checks the trusted
    # X-Forwarded-Proto header (not the raw connection) to decide whether a request is already
    # HTTPS, so this won't redirect-loop behind Cloudflare/nginx.
    SECURE_SSL_REDIRECT = True
    SECURE_HSTS_SECONDS = 63072000
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True

The one-line comment matters more than it looks. SECURE_SSL_REDIRECT = True behind a reverse proxy is a classic way to create an infinite redirect loop — Django sees the internal connection from nginx, which is plain HTTP, and redirects a request that was already HTTPS at the edge. The fix is SECURE_PROXY_SSL_HEADER, already set earlier in the same file for the secure-cookie logic to work correctly:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

With that in place, Django trusts nginx's X-Forwarded-Proto header instead of the raw socket, so the redirect only fires for a request that's genuinely still plaintext somewhere in the chain.

Before trusting this, I ran the full test suite — 671 tests — to make sure the new SECURE_SSL_REDIRECT didn't start redirecting Django's own test client mid-suite (a real risk: if not DEBUG blocks activate whenever DEBUG is falsy, and plenty of test runners force that). Locally, DEBUG stays True from .env, same as it always has for every other setting in that block — all 671 passed unchanged.

Making It Stick — Item 15

The checklist's own final item was the most legitimate one: nothing was running check --deploy anywhere except by hand, whenever someone remembered to. Fixed with a small GitHub Actions workflow:

- name: Run production-readiness check (manage.py check --deploy)
  working-directory: backend
  env:
    DEBUG: "False"
  run: python manage.py check --deploy

Running alongside the actual pytest suite on every push to develop and main, against a real Postgres service container — matching the same "verify in CI, not by hand, not by trusting a report" instinct that shaped the Trivy integration earlier. A regression here now fails a pull request instead of surfacing three weeks later in someone's audit screenshot.

The Actual Lesson

A security checklist with fifteen items and thirteen red marks looks urgent — that's exactly what makes it worth verifying before reacting to it. Django already has an authoritative answer to "is this deployment configured correctly," built into the framework, free, and it takes ten seconds to run. When a report disagrees with what the tool itself says, the tool wins — not because checklists are worthless, but because a checklist is a claim about your config, and manage.py check --deploy is your config, read directly.

If you're staring at a security audit for a Django app and something on it feels off, the fastest way to find out is the same one command: python manage.py check --deploy. If you want a second opinion on what it turns up, get in touch.

Related Articles

Security

Rate Limiting, Token Blacklisting, and Admin MFA — Auth Hardening for Tabeer.ai

Part two of the production-readiness checklist: authentication and account security. Five of fifteen items were genuinely missing — rate limiting, real logout, admin MFA, consent records, and self-service data export/deletion — and here's exactly how each got fixed without a new dependency for most of them.

10 min read
Security

The Encryption Checklist for a Small Django Production Stack

Thirteen encryption checks, framed as 'encryption at multiple layers, not just HTTPS.' Most of the layers already existed — TLS to Postgres, client-side-encrypted backups — one item doesn't apply to this architecture at all, and two needed an AWS console check I couldn't run myself.

7 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project