Django Production Security Checklist and Hardening
Harden your Django app for production — security settings, CSRF, XSS, SQL injection, HTTPS, headers, secrets, and deployment checks.
What you'll learn
- ✓How to run Django's built-in security check command
- ✓How to configure HTTPS, HSTS, and secure cookies
- ✓How Django protects against CSRF, XSS, and SQL injection
- ✓How to manage secrets and environment variables safely
- ✓How to set security headers and Content Security Policy
Prerequisites
- •A Django project ready for production deployment
- •Basic understanding of HTTP and web security concepts
Django ships with strong security defaults, but defaults aren’t enough for production. A misconfigured DEBUG = True, a leaked SECRET_KEY, or a missing HTTPS redirect can expose your entire application. This guide is a practical checklist — go through each section before every production deployment.
Start With Django’s Built-In Check
Django has a check --deploy command that scans your settings for common security misconfigurations:
python manage.py check --deploy
This reports issues like DEBUG = True, missing SECURE_SSL_REDIRECT, weak SECRET_KEY, and more. Fix every warning before deploying.
The Critical Settings
DEBUG = False
This is the single most important setting. With DEBUG = True:
- Full tracebacks with source code are shown to users
- The 404 page lists every URL pattern
- Database queries are stored in memory (memory leak)
# settings.py
DEBUG = False # NEVER True in production
SECRET_KEY From Environment
The SECRET_KEY signs sessions, CSRF tokens, and password reset links. If leaked, an attacker can forge any of these.
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # Crash if missing, not silently use a default
Generate a strong key:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Never commit SECRET_KEY to version control. Use environment variables, a secrets manager (AWS Secrets Manager, HashiCorp Vault), or a .env file excluded from git.
ALLOWED_HOSTS
ALLOWED_HOSTS = ["example.com", "www.example.com"]
Never use ["*"] in production. This setting prevents HTTP Host header attacks. Without it, attackers can poison password reset emails and cache entries.
HTTPS and HSTS
Force HTTPS
SECURE_SSL_REDIRECT = True # Redirect all HTTP to HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # If behind a proxy/load balancer
HTTP Strict Transport Security
HSTS tells browsers to only use HTTPS for your domain, even if the user types http://:
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Start with a short SECURE_HSTS_SECONDS (3600) and increase once you’ve confirmed everything works over HTTPS. HSTS is hard to undo — browsers cache it.
Cookie Security
# Session cookie
SESSION_COOKIE_SECURE = True # Only send over HTTPS
SESSION_COOKIE_HTTPONLY = True # JavaScript can't read it
SESSION_COOKIE_SAMESITE = "Lax" # Prevent CSRF via cross-site requests
SESSION_COOKIE_AGE = 1209600 # 2 weeks
# CSRF cookie
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"
HTTPONLY prevents JavaScript from reading the cookie — mitigates XSS-based session theft. SAMESITE = "Lax" prevents the browser from sending cookies on cross-origin POST requests.
CSRF Protection
Django’s CSRF middleware is enabled by default. Keep it enabled and follow these rules:
MIDDLEWARE = [
# ...
"django.middleware.csrf.CsrfViewMiddleware", # Don't remove this
# ...
]
In templates, always use the {% csrf_token %} tag in forms:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
For AJAX requests, include the CSRF token in the header:
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch("/api/data/", {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
Never use @csrf_exempt on views unless you have a strong reason (like a webhook endpoint with its own authentication).
XSS Protection
Django’s template engine auto-escapes variables by default. The {{ variable }} syntax escapes HTML characters:
# If variable = '<script>alert("xss")</script>'
# Template renders: <script>alert("xss")</script>
Never use {{ variable|safe }} or {% autoescape off %} on user-supplied content. If you must render HTML, sanitize it server-side with a library like bleach:
import bleach
ALLOWED_TAGS = ["p", "br", "strong", "em", "a", "ul", "ol", "li"]
ALLOWED_ATTRS = {"a": ["href", "title"]}
clean_html = bleach.clean(user_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
Enable the browser-side XSS protection header:
SECURE_BROWSER_XSS_FILTER = True # Sets X-XSS-Protection header
SQL Injection Protection
Django’s ORM parameterizes all queries, preventing SQL injection by default:
# Safe — parameterized query
User.objects.filter(username=user_input)
# Safe — parameterized
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [user_input])
Never interpolate user input into raw SQL:
# DANGEROUS — SQL injection vulnerability
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{user_input}'")
# DANGEROUS
cursor.execute(f"SELECT * FROM auth_user WHERE username = '{user_input}'")
If you must use raw SQL, always use parameterized queries with %s placeholders and pass values as a list.
Security Headers
Content-Type Sniffing
SECURE_CONTENT_TYPE_NOSNIFF = True # Sets X-Content-Type-Options: nosniff
Prevents browsers from guessing the content type — stops attacks where an uploaded file is interpreted as HTML.
Clickjacking Protection
X_FRAME_OPTIONS = "DENY" # Prevents your site from being embedded in iframes
Use "SAMEORIGIN" if you need iframes within your own site.
Content Security Policy
CSP is the most powerful XSS defense. Use django-csp:
pip install django-csp
# settings.py
MIDDLEWARE = [
"csp.middleware.CSPMiddleware",
# ...
]
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'") # Tighten if possible
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
Start with CSP_REPORT_ONLY = True to log violations without breaking your site, then switch to enforcement mode.
Password Security
Django’s password hashers are strong by default (PBKDF2 with 870,000 iterations as of Django 5.x). You can strengthen them:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher", # Strongest, needs argon2-cffi
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
]
Install Argon2:
pip install argon2-cffi
Enforce password strength with validators:
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 10}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
File Upload Security
Uploaded files can be vectors for attack. Never trust the file extension or content type from the client:
# Validate file type server-side
import magic
def validate_file(uploaded_file):
mime = magic.from_buffer(uploaded_file.read(2048), mime=True)
uploaded_file.seek(0)
allowed = ["image/jpeg", "image/png", "application/pdf"]
if mime not in allowed:
raise ValidationError(f"File type {mime} is not allowed")
Serve uploaded files from a separate domain or CDN to prevent cookie theft. Never serve user uploads from the same origin as your application.
# Use a separate storage backend
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_S3_CUSTOM_DOMAIN = "media.example.com"
Secrets Management
Never hardcode secrets. Use environment variables with a library like python-decouple or django-environ:
# settings.py
import environ
env = environ.Env()
environ.Env.read_env() # reads .env file
SECRET_KEY = env("DJANGO_SECRET_KEY")
DATABASE_URL = env("DATABASE_URL")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY")
Add .env to .gitignore:
# .gitignore
.env
*.env
Production Deployment Checklist
Run through this before every deployment:
# All of these should be set in production
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["yourdomain.com"]
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_BROWSER_XSS_FILTER = True
Then run the deploy check:
python manage.py check --deploy
Fix every warning. Django’s security middleware does a lot of heavy lifting, but only if you configure it correctly.
Summary
Django’s security is strong out of the box, but production requires explicit configuration. Set DEBUG = False, load secrets from environment variables, enforce HTTPS with HSTS, secure your cookies, and add a Content Security Policy. Use python manage.py check --deploy as your gate before every release. Security isn’t a feature you add once — it’s a configuration you verify continuously.
Related articles
- Django Django + Celery: Background Tasks and Task Queues
Integrate Celery with Django for background task processing — setup, writing tasks, retries, periodic tasks, and monitoring with Flower.
- Django Django Custom Managers and Chainable QuerySets
Build custom managers and chainable QuerySets in Django to encapsulate query logic, keep views thin, and write expressive ORM code.
- Django Writing Custom Middleware in Django
Build custom Django middleware — request/response processing, exception handling, middleware ordering, and real-world examples.
- Django Django Multi-Tenancy: Shared Database Schema Patterns
Implement multi-tenancy in Django using shared database schemas — tenant models, middleware, filtered QuerySets, and data isolation.