Skip to main content

Security Settings

Production Security Critical

Proper security configuration is critical for production deployments. Always use:

  • ✅ Strong SECRET_KEY (50+ characters)
  • ✅ Reverse proxy with SSL/TLS (nginx, Cloudflare, traefik)
  • ✅ Specific security_domains for production (never use ['*'])
  • ✅ Environment variables for secrets

Django-CFG provides automatic security configuration based on your domains and environment.

Overview

Environment-Aware Security

Django-CFG provides environment-aware security - development is fully open for convenience, production is strict and secure.

How it works:

Development Mode (debug=True or no security_domains):

  • CORS - Fully open (CORS_ALLOW_ALL_ORIGINS=True)
  • ALLOWED_HOSTS - Accepts all (['*'])
  • Docker - Automatic Docker IP support
  • Localhost - All ports allowed

Production Mode (when security_domains specified):

  • ALLOWED_HOSTS - Generated from security_domains
  • CORS - Strict whitelist with credentials
  • CSRF - Trusted origins from domains
  • Security headers - Automatic configuration
  • SSL - Assumes reverse proxy (nginx/Cloudflare)

security_domains Field

The security_domains field is the foundation of security configuration:

from django_cfg import DjangoConfig

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False

# Production domains (flexible format - Django-CFG normalizes automatically)
security_domains: list = [
"myapp.com", # ✅ No protocol
"https://api.myapp.com", # ✅ With protocol
"admin.myapp.com:8443", # ✅ With port
]

# Development: security_domains optional (CORS fully open by default)
class DevConfig(DjangoConfig):
debug: bool = True
# security_domains not needed - auto-configured for development

Auto-Generated Settings

From security_domains, Django-CFG automatically generates:

Development Mode (no security_domains):

# CORS fully open
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = False

# All hosts accepted
ALLOWED_HOSTS = ['*']

# CSRF only for popular dev ports
CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000',
'http://localhost:5173',
# ... 7 popular dev ports
]

Production Mode (security_domains specified):

# Strict CORS from security_domains
CORS_ALLOWED_ORIGINS = [
'https://myapp.com',
'https://api.myapp.com',
'https://admin.myapp.com',
]
CORS_ALLOW_CREDENTIALS = True

# Hosts from security_domains + Docker IPs (if detected)
ALLOWED_HOSTS = [
'myapp.com',
'api.myapp.com',
'admin.myapp.com',
# Docker IPs added automatically if /.dockerenv detected
'r"172\.(1[6-9]|2[0-9]|3[0-1])\..*', # Docker range
]

# CSRF from security_domains
CSRF_TRUSTED_ORIGINS = [
'https://myapp.com',
'https://api.myapp.com',
'https://admin.myapp.com',
]

SSL/TLS Configuration

Reverse Proxy Best Practice

Django-CFG assumes SSL/TLS termination happens at the reverse proxy level (nginx, Cloudflare, traefik, AWS ALB). This is the industry-standard approach.

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

# ssl_redirect not specified - Django-CFG defaults to None (disabled)
# SSL handled by reverse proxy (nginx, Cloudflare, etc.)

Default behavior:

  • SECURE_SSL_REDIRECT = False - Reverse proxy handles redirects
  • SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - Trust proxy headers
  • SESSION_COOKIE_SECURE = True - Secure cookies in production
  • CSRF_COOKIE_SECURE = True - Secure CSRF tokens

Explicit SSL Redirect (Rare)

Only for Bare Metal

Set ssl_redirect=True ONLY if Django handles SSL directly without a reverse proxy. This is rare in modern deployments.

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

# Explicit SSL redirect - ONLY if Django handles SSL directly
ssl_redirect: bool = True

When to use:

  • Don't use with nginx/Cloudflare/traefik (causes redirect loops)
  • Use only for bare metal Django with direct SSL certificates
  • Use only for testing SSL redirects in development

CORS Configuration

Automatic CORS

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

Automatic CORS headers:

CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]

Custom CORS Headers

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

# Add custom headers
cors_allow_headers: list = [
'x-api-key',
'x-custom-header',
]

Result: Merges default headers + custom headers

CORS for API

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["api.myapp.com", "myapp.com"]

cors_allow_headers: list = [
'x-api-key',
'authorization',
]

Generated settings:

CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = [
'https://api.myapp.com',
'https://www.api.myapp.com',
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000',
]

Complete Security Example

Production Configuration

# config.py
from django_cfg import DjangoConfig

class ProductionConfig(DjangoConfig):
secret_key: str = "your-production-secret-key-minimum-50-characters"
debug: bool = False
project_name: str = "My Production App"

# Security domains (flexible format - auto-normalized)
security_domains: list = [
"myapp.com", # ✅ No protocol
"https://api.myapp.com", # ✅ With protocol
"admin.myapp.com:8443", # ✅ With port
]

# ssl_redirect: Optional - defaults to None (reverse proxy handles SSL)
# CORS auto-configured from domains
# ALLOWED_HOSTS auto-generated

Generated security settings:

# ALLOWED_HOSTS (from security_domains + Docker IPs if detected)
ALLOWED_HOSTS = [
'myapp.com',
'api.myapp.com',
'admin.myapp.com',
# Docker IPs added automatically if /.dockerenv exists:
'r"172\.(1[6-9]|2[0-9]|3[0-1])\..*', # Docker bridge
'r"192\.168\..*', # Docker compose
]

# SSL/TLS (assumes reverse proxy)
SECURE_SSL_REDIRECT = False # Reverse proxy handles redirect
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# CORS (strict whitelist with credentials)
CORS_ALLOWED_ORIGINS = [
'https://myapp.com',
'https://api.myapp.com',
'https://admin.myapp.com',
]
CORS_ALLOW_CREDENTIALS = True

# CSRF (from security_domains)
CSRF_TRUSTED_ORIGINS = [
'https://myapp.com',
'https://api.myapp.com',
'https://admin.myapp.com',
]

# Security headers
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'

Development Configuration

# config.py
from django_cfg import DjangoConfig

class DevelopmentConfig(DjangoConfig):
secret_key: str = "dev-secret-key-minimum-50-characters-long-string"
debug: bool = True
project_name: str = "My Dev App"

# security_domains: Optional - not needed in development
# Django-CFG auto-configures for development convenience

Generated security settings:

# CORS (fully open for development)
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = False # Must be False with ALLOW_ALL

# ALLOWED_HOSTS (accept everything)
ALLOWED_HOSTS = ['*']

# CSRF (popular dev ports only)
CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000', # React/Next.js
'http://localhost:5173', # Vite
'http://localhost:8080', # Webpack
'http://localhost:4200', # Angular
# ... 7 popular ports
]

# SSL/TLS (disabled in development)
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False

Environment-Specific Security

Using Environment Detection

from django_cfg import DjangoConfig

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
project_name: str = "My App"

@property
def debug(self) -> bool:
return self._environment == "development"

@property
def security_domains(self) -> list:
if self._environment == "production":
return ["myapp.com", "api.myapp.com"]
elif self._environment == "staging":
return ["staging.myapp.com"]
return [] # Development

@property
def ssl_redirect(self) -> bool:
return self._environment in ["production", "staging"]

Using YAML Configuration

# config.production.yaml
secret_key: "${SECRET_KEY}" # From environment
debug: false
security_domains:
- myapp.com # ✅ Flexible format
- https://api.myapp.com # ✅ With protocol
- admin.myapp.com:8443 # ✅ With port

# ssl_redirect: Optional - not needed (defaults to reverse proxy mode)

# config.development.yaml
secret_key: "dev-secret-key-minimum-50-chars"
debug: true

# security_domains: Optional - not needed in development
# Django-CFG auto-configures CORS fully open for development
# config.py
from django_cfg import load_config

config = load_config() # Loads environment-specific YAML

Security Best Practices

Security Checklist

Follow all these practices for production security. Skipping any can expose your application to attacks.

1. Always Set Strong SECRET_KEY

Weak SECRET_KEY

Never use:

  • Default values like "changeme" or "insecure-key"
  • Short keys (< 50 characters)
  • Predictable patterns or dictionary words
  • Keys committed to version control

Consequences:

  • Session hijacking
  • CSRF token forgery
  • Password reset token prediction
  • Data tampering

❌ Bad:

secret_key: str = "changeme"  # ❌ NEVER DO THIS
secret_key: str = "django-insecure-key" # ❌ Too weak

✅ Good:

# Use environment variable
secret_key: str = os.environ["SECRET_KEY"] # ✅ Secure

# Or generate new key:
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())
Key Generation

Generate a cryptographically secure key:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Security Headers Reference

Django-CFG automatically configures security headers based on environment:

Production Headers

SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

Development Headers

SECURE_CONTENT_TYPE_NOSNIFF = False
SECURE_BROWSER_XSS_FILTER = False
X_FRAME_OPTIONS = 'SAMEORIGIN'

Troubleshooting

ALLOWED_HOSTS Error

Error:

DisallowedHost at /
Invalid HTTP_HOST header: 'newdomain.com'

Solution:

class MyConfig(DjangoConfig):
security_domains: list = [
"myapp.com",
"newdomain.com", # Add new domain
]

CORS Error

Error:

Access to fetch at 'https://api.myapp.com' from origin 'https://myapp.com'
has been blocked by CORS policy

Solution:

class MyConfig(DjangoConfig):
security_domains: list = [
"myapp.com",
"api.myapp.com", # Add API domain
]

SSL Redirect Loop

Problem: Infinite redirect loop in production

Solution:

class MyConfig(DjangoConfig):
security_domains: list = ["myapp.com"]
ssl_redirect: bool = True

# Add to settings:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

Django-Axes: Brute-Force Protection

Automatic Brute-Force Protection

Django-CFG includes django-axes for automatic brute-force attack protection. It tracks failed login attempts and locks out attackers.

Overview

Django-Axes provides:

  • Failed login tracking - Monitor failed authentication attempts
  • Automatic lockout - Block users/IPs after too many failures
  • Time-based cooldown - Automatic unlock after cooldown period
  • IP + Username tracking - Flexible tracking strategies
  • Proxy/Cloudflare support - Real IP extraction from headers
  • Admin integration - View and manage lockouts in admin panel

Smart Defaults

Django-CFG provides environment-aware defaults for django-axes:

Development Mode:

AXES_ENABLED = True
AXES_FAILURE_LIMIT = 10 # More attempts allowed in dev
AXES_COOLOFF_TIME = 1 # 1 hour lockout
AXES_VERBOSE = True # Detailed logging

Production Mode:

AXES_ENABLED = True
AXES_FAILURE_LIMIT = 5 # Stricter limit in production
AXES_COOLOFF_TIME = 24 # 24 hours lockout
AXES_VERBOSE = False # Less verbose logging
No Configuration Required

Django-Axes works out of the box with smart defaults. You only need to configure it if you want custom behavior.

Basic Configuration

from django_cfg import DjangoConfig

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

# axes: No configuration needed - smart defaults used automatically

Result: Automatic brute-force protection with environment-aware defaults.

Configuration Options

Basic Settings

from django_cfg import AxesConfig

axes = AxesConfig(
# Enable/disable axes
enabled=True, # Default: True

# Failure limit (None = auto: 10 dev, 5 prod)
failure_limit=5,

# Cooloff time in hours (None = auto: 1 dev, 24 prod)
cooloff_time=24,

# Lock out after reaching failure limit
lock_out_at_failure=True, # Default: True

# Reset failure count after successful login
reset_on_success=True, # Default: True
)

UI/UX Settings

axes = AxesConfig(
# Custom lockout template
lockout_template="account/locked.html", # Default: None (built-in response)

# Custom lockout URL redirect
lockout_url="/account/locked/", # Default: None (built-in response)
)

Logging Settings

axes = AxesConfig(
# Verbose logging (None = auto: True dev, False prod)
verbose=True,

# Log access failures for security audit
enable_access_failure_log=True, # Default: True
)

IP Whitelist/Blacklist

axes = AxesConfig(
# IP addresses that bypass axes protection
allowed_ips=[
'192.168.1.100', # Office IP
'10.0.0.5', # Admin IP
],

# IP addresses that are always blocked
denied_ips=[
'10.0.0.2', # Known attacker
],
)

Proxy/Cloudflare Support

Real IP Extraction

Django-Axes automatically extracts real client IPs from proxy headers (nginx, Cloudflare, traefik).

Default Configuration

axes = AxesConfig(
# Number of proxies between client and server
ipware_proxy_count=1, # Default: 1

# Order of headers to extract real IP
ipware_meta_precedence_order=[
'HTTP_X_FORWARDED_FOR', # Cloudflare, nginx, traefik
'HTTP_X_REAL_IP', # Alternative proxy header
'REMOTE_ADDR', # Fallback to direct connection
],
)

Cloudflare Configuration

# For Cloudflare
axes = AxesConfig(
ipware_proxy_count=1, # Cloudflare is 1 proxy layer
ipware_meta_precedence_order=[
'HTTP_X_FORWARDED_FOR',
'HTTP_CF_CONNECTING_IP', # Cloudflare-specific header
'REMOTE_ADDR',
],
)

Multiple Proxies

# For multiple proxies (e.g., Cloudflare + nginx)
axes = AxesConfig(
ipware_proxy_count=2, # 2 proxy layers
ipware_meta_precedence_order=[
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR',
],
)

Custom Lockout Page

1. Create lockout template:

templates/account/locked.html
{% extends "base.html" %}

{% block content %}
<div class="lockout-page">
<h1>Account Locked</h1>
<p>
Your account has been locked due to too many failed login attempts.
Please try again in {{ cooloff_time }} hours.
</p>
<p>
If you believe this is a mistake, please contact support.
</p>
</div>
{% endblock %}

2. Configure axes:

axes = AxesConfig(
lockout_template="account/locked.html",
)

Admin Integration

Django-Axes provides admin interface to view and manage lockouts:

# Admin panel automatically includes:
# - Access Attempts (all login attempts)
# - Access Logs (successful logins)
# - Access Failures (failed login attempts)

Admin features:

  • ✅ View all failed login attempts
  • ✅ View locked accounts/IPs
  • ✅ Manually unlock accounts
  • ✅ View attempt history
  • ✅ Search by username/IP

Production Examples

Strict Security

from django_cfg import DjangoConfig, AxesConfig

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = False
security_domains: list = ["myapp.com"]

# Very strict brute-force protection
axes: AxesConfig = AxesConfig(
failure_limit=3, # Only 3 attempts
cooloff_time=72, # 72 hours lockout (3 days)
lockout_template="account/locked.html",
enable_access_failure_log=True,
)

Moderate Security

axes: AxesConfig = AxesConfig(
failure_limit=5, # 5 attempts (default production)
cooloff_time=24, # 24 hours lockout
reset_on_success=True,
)

Whitelist Admin IPs

axes: AxesConfig = AxesConfig(
failure_limit=5,
cooloff_time=24,

# Whitelist office/admin IPs
allowed_ips=[
'192.168.1.0/24', # Office network
'10.0.0.100', # Admin IP
],
)

Cloudflare + nginx Setup

axes: AxesConfig = AxesConfig(
failure_limit=5,
cooloff_time=24,

# Cloudflare + nginx proxy setup
ipware_proxy_count=2,
ipware_meta_precedence_order=[
'HTTP_CF_CONNECTING_IP', # Cloudflare real IP
'HTTP_X_FORWARDED_FOR', # nginx proxy
'HTTP_X_REAL_IP',
'REMOTE_ADDR',
],
)

Development vs Production

class DevConfig(DjangoConfig):
debug: bool = True

# axes: Optional - smart defaults provide relaxed settings
# AXES_FAILURE_LIMIT = 10 (more attempts)
# AXES_COOLOFF_TIME = 1 (1 hour)
# AXES_VERBOSE = True (detailed logging)

Monitoring Lockouts

Check Lockout Status

from axes.helpers import get_lockout_response

# Check if IP/username is locked
lockout_response = get_lockout_response(request)
if lockout_response:
# User is locked out
return lockout_response

View Lockout Attempts

from axes.models import AccessAttempt

# Get all lockouts
lockouts = AccessAttempt.objects.filter(failures_since_start__gte=5)

# Get lockouts for specific IP
ip_lockouts = AccessAttempt.objects.filter(ip_address='192.168.1.100')

# Get lockouts for specific username
user_lockouts = AccessAttempt.objects.filter(username='admin')

Troubleshooting

False Lockouts

Problem: Legitimate users getting locked out

Solutions:

  1. Increase failure limit:
axes = AxesConfig(
failure_limit=10, # More lenient
)
  1. Whitelist trusted IPs:
axes = AxesConfig(
allowed_ips=['192.168.1.0/24'], # Office network
)

Proxy Issues

Problem: All requests appear to come from proxy IP

Solution: Configure proxy settings correctly

axes = AxesConfig(
# Adjust proxy count
ipware_proxy_count=1, # Or 2 for Cloudflare + nginx

# Add proxy-specific headers
ipware_meta_precedence_order=[
'HTTP_CF_CONNECTING_IP', # For Cloudflare
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR',
],
)

Unlock Users

Option 1: Admin Panel

  1. Go to Admin → Axes → Access Attempts
  2. Find locked user/IP
  3. Delete the attempt record

Option 2: Management Command

# Unlock specific username
python manage.py axes_reset username admin

# Unlock specific IP
python manage.py axes_reset ip 192.168.1.100

# Clear all lockouts
python manage.py axes_reset

Advanced Settings

Cache Backend

axes = AxesConfig(
cache_name='default', # Use 'redis' for Redis cache
)

Custom Username Field

axes = AxesConfig(
username_form_field='email', # Track by email instead of username
)

Security Best Practices

Axes Security Tips
  1. ✅ Always enable axes in production (enabled=True)
  2. ✅ Use strict failure limits (3-5 attempts)
  3. ✅ Configure proxy settings correctly for Cloudflare/nginx
  4. ✅ Whitelist admin/office IPs to prevent self-lockout
  5. ✅ Monitor lockout logs for attack patterns
  6. ✅ Enable access failure logging (enable_access_failure_log=True)

See Also