HTTPS (HTTP Secure) is essential for protecting data in transit and ensuring the integrity and confidentiality of communications between clients and servers. This chapter covers implementing HTTPS in Django applications and configuring HTTP Strict Transport Security (HSTS) for enhanced security.
# Without HTTPS, sensitive data is transmitted in plain text:
# HTTP Request (VISIBLE TO ANYONE ON THE NETWORK):
"""
POST /login/ HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=admin&password=secretpassword123&csrfmiddlewaretoken=abc123
"""
# HTTPS Request (ENCRYPTED):
"""
POST /login/ HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
[ENCRYPTED DATA - UNREADABLE TO EAVESDROPPERS]
"""
# Security benefits of HTTPS:
# 1. Encryption - Data is encrypted in transit
# 2. Authentication - Verifies server identity
# 3. Integrity - Prevents data tampering
# 4. Trust - Required for modern web features
# Certificate types and their use cases:
# 1. Domain Validated (DV) Certificates
# - Validates domain ownership only
# - Suitable for most websites
# - Quick issuance (minutes to hours)
# 2. Organization Validated (OV) Certificates
# - Validates organization identity
# - Shows organization name in certificate
# - More trust indicators
# 3. Extended Validation (EV) Certificates
# - Highest level of validation
# - Shows organization name in browser
# - Most expensive but highest trust
# 4. Wildcard Certificates
# - Covers all subdomains (*.example.com)
# - Useful for multiple subdomains
# - Single certificate management
# 5. Multi-Domain (SAN) Certificates
# - Covers multiple different domains
# - Cost-effective for multiple sites
# - Single certificate for multiple domains
# settings.py - Basic HTTPS configuration
import os
# Force HTTPS in production
SECURE_SSL_REDIRECT = True # Redirect HTTP to HTTPS
# Secure cookie settings
SESSION_COOKIE_SECURE = True # Send session cookies over HTTPS only
CSRF_COOKIE_SECURE = True # Send CSRF cookies over HTTPS only
# Secure headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# HSTS (HTTP Strict Transport Security)
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Proxy configuration (if behind load balancer/proxy)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Referrer policy
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# settings/base.py - Base security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# settings/development.py - Development settings
from .base import *
# Allow HTTP in development
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SECURE_HSTS_SECONDS = 0 # Disable HSTS in development
# settings/production.py - Production settings
from .base import *
# Enforce HTTPS in production
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# HSTS configuration
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Proxy configuration for production
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Additional production security
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# middleware.py - Custom HTTPS enforcement middleware
from django.http import HttpResponsePermanentRedirect
from django.conf import settings
from django.urls import reverse
import logging
logger = logging.getLogger('security')
class EnhancedHTTPSMiddleware:
"""Enhanced HTTPS enforcement with additional security features"""
def __init__(self, get_response):
self.get_response = get_response
self.exempt_paths = getattr(settings, 'HTTPS_EXEMPT_PATHS', [])
self.force_https_paths = getattr(settings, 'FORCE_HTTPS_PATHS', [])
def __call__(self, request):
# Check if HTTPS should be enforced
if self.should_enforce_https(request):
if not request.is_secure():
return self.redirect_to_https(request)
response = self.get_response(request)
# Add security headers for HTTPS responses
if request.is_secure():
self.add_security_headers(request, response)
return response
def should_enforce_https(self, request):
"""Determine if HTTPS should be enforced for this request"""
# Always enforce for sensitive paths
if any(request.path.startswith(path) for path in self.force_https_paths):
return True
# Skip enforcement for exempt paths
if any(request.path.startswith(path) for path in self.exempt_paths):
return False
# Check global setting
return getattr(settings, 'SECURE_SSL_REDIRECT', False)
def redirect_to_https(self, request):
"""Redirect HTTP request to HTTPS"""
# Log HTTP access attempt
logger.info(f"HTTP request redirected to HTTPS: {request.path}", extra={
'ip_address': self.get_client_ip(request),
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'path': request.path,
})
# Build HTTPS URL
https_url = f"https://{request.get_host()}{request.get_full_path()}"
return HttpResponsePermanentRedirect(https_url)
def add_security_headers(self, request, response):
"""Add security headers to HTTPS responses"""
# HSTS header
if getattr(settings, 'SECURE_HSTS_SECONDS', 0):
hsts_header = f"max-age={settings.SECURE_HSTS_SECONDS}"
if getattr(settings, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', False):
hsts_header += "; includeSubDomains"
if getattr(settings, 'SECURE_HSTS_PRELOAD', False):
hsts_header += "; preload"
response['Strict-Transport-Security'] = hsts_header
# Additional security headers
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
# Referrer policy
if hasattr(settings, 'SECURE_REFERRER_POLICY'):
response['Referrer-Policy'] = settings.SECURE_REFERRER_POLICY
def get_client_ip(self, request):
"""Get client IP address"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
# settings.py - Configuration for custom middleware
MIDDLEWARE = [
'myapp.middleware.EnhancedHTTPSMiddleware',
# ... other middleware
]
# Custom HTTPS settings
HTTPS_EXEMPT_PATHS = [
'/health/', # Health check endpoints
'/status/', # Status endpoints
]
FORCE_HTTPS_PATHS = [
'/login/',
'/admin/',
'/api/auth/',
'/payment/',
]
# Certificate management with Let's Encrypt
# Using certbot for automatic certificate management
# Install certbot
# pip install certbot certbot-nginx # or certbot-apache
# Automatic certificate generation and renewal
"""
# Generate certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Test renewal
sudo certbot renew --dry-run
# Automatic renewal (add to crontab)
0 12 * * * /usr/bin/certbot renew --quiet
"""
# Django settings for Let's Encrypt
# settings.py
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# ACME challenge handling (for certificate validation)
ACME_CHALLENGE_PATH = '/.well-known/acme-challenge/'
# URL configuration for ACME challenges
# urls.py
from django.urls import path
from django.views.static import serve
from django.conf import settings
import os
urlpatterns = [
# ... your URL patterns
]
# Serve ACME challenge files
if not settings.DEBUG:
urlpatterns += [
path('.well-known/acme-challenge/<path:path>',
serve,
{'document_root': '/var/www/html/.well-known/acme-challenge/'}),
]
# Certificate monitoring and alerting
import ssl
import socket
from datetime import datetime, timedelta
from django.core.management.base import BaseCommand
from django.core.mail import send_mail
class Command(BaseCommand):
"""Monitor SSL certificate expiration"""
help = 'Check SSL certificate expiration and send alerts'
def add_arguments(self, parser):
parser.add_argument('--domain', type=str, help='Domain to check')
parser.add_argument('--port', type=int, default=443, help='Port to check')
parser.add_argument('--days', type=int, default=30, help='Days before expiration to alert')
def handle(self, *args, **options):
domain = options['domain'] or 'yourdomain.com'
port = options['port']
alert_days = options['days']
try:
# Get certificate information
cert_info = self.get_certificate_info(domain, port)
# Check expiration
expires_in_days = self.check_expiration(cert_info)
if expires_in_days <= alert_days:
self.send_expiration_alert(domain, expires_in_days, cert_info)
self.stdout.write(
self.style.WARNING(
f'Certificate for {domain} expires in {expires_in_days} days'
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f'Certificate for {domain} is valid for {expires_in_days} days'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Error checking certificate for {domain}: {e}')
)
def get_certificate_info(self, domain, port):
"""Get SSL certificate information"""
context = ssl.create_default_context()
with socket.create_connection((domain, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:
cert = ssock.getpeercert()
return cert
def check_expiration(self, cert_info):
"""Check certificate expiration"""
# Parse expiration date
not_after = cert_info['notAfter']
expiry_date = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z')
# Calculate days until expiration
days_until_expiry = (expiry_date - datetime.now()).days
return days_until_expiry
def send_expiration_alert(self, domain, days_remaining, cert_info):
"""Send certificate expiration alert"""
subject = f'SSL Certificate Expiring Soon: {domain}'
message = f"""
The SSL certificate for {domain} will expire in {days_remaining} days.
Certificate Details:
- Subject: {cert_info.get('subject', 'Unknown')}
- Issuer: {cert_info.get('issuer', 'Unknown')}
- Expires: {cert_info.get('notAfter', 'Unknown')}
Please renew the certificate before it expires to avoid service disruption.
"""
send_mail(
subject,
message,
'ssl-monitor@yourdomain.com',
['admin@yourdomain.com'],
fail_silently=False,
)
# Run certificate monitoring
# python manage.py check_ssl_certificate --domain yourdomain.com --days 30
# settings.py - HSTS configuration
# HSTS tells browsers to only connect via HTTPS
# Basic HSTS configuration
SECURE_HSTS_SECONDS = 31536000 # 1 year (recommended minimum)
# Include subdomains in HSTS policy
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# Enable HSTS preloading (submit to browser preload lists)
SECURE_HSTS_PRELOAD = True
# Progressive HSTS deployment
# Start with shorter duration, then increase
if DEBUG:
SECURE_HSTS_SECONDS = 0 # Disable in development
else:
# Production HSTS progression:
# Week 1: 300 (5 minutes) - for testing
# Week 2: 86400 (1 day) - short term
# Week 3: 604800 (1 week) - medium term
# Week 4+: 31536000 (1 year) - long term
SECURE_HSTS_SECONDS = 31536000
# middleware.py - Custom HSTS middleware with advanced features
class AdvancedHSTSMiddleware:
"""Advanced HSTS middleware with conditional policies"""
def __init__(self, get_response):
self.get_response = get_response
# HSTS configuration
self.hsts_config = {
'default': {
'max_age': 31536000, # 1 year
'include_subdomains': True,
'preload': True
},
'api': {
'max_age': 63072000, # 2 years (stricter for API)
'include_subdomains': True,
'preload': True
},
'admin': {
'max_age': 63072000, # 2 years (stricter for admin)
'include_subdomains': True,
'preload': True
}
}
def __call__(self, request):
response = self.get_response(request)
# Only add HSTS header for HTTPS requests
if request.is_secure():
hsts_header = self.build_hsts_header(request)
if hsts_header:
response['Strict-Transport-Security'] = hsts_header
return response
def build_hsts_header(self, request):
"""Build HSTS header based on request path"""
# Determine HSTS policy based on path
if request.path.startswith('/api/'):
config = self.hsts_config['api']
elif request.path.startswith('/admin/'):
config = self.hsts_config['admin']
else:
config = self.hsts_config['default']
# Build header
header_parts = [f"max-age={config['max_age']}"]
if config.get('include_subdomains'):
header_parts.append('includeSubDomains')
if config.get('preload'):
header_parts.append('preload')
return '; '.join(header_parts)
# HSTS preload list submission
"""
To submit your domain to HSTS preload lists:
1. Ensure HSTS is properly configured with preload directive
2. Visit https://hstspreload.org/
3. Submit your domain
4. Wait for inclusion in browser preload lists
Requirements for preload:
- Serve a valid certificate
- Redirect all HTTP traffic to HTTPS
- Serve HSTS header with:
- max-age of at least 31536000 (1 year)
- includeSubDomains directive
- preload directive
"""
# HSTS monitoring and compliance checking
import requests
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Check HSTS compliance and configuration"""
help = 'Check HSTS header configuration and compliance'
def add_arguments(self, parser):
parser.add_argument('--url', type=str, help='URL to check')
def handle(self, *args, **options):
url = options['url'] or 'https://yourdomain.com'
try:
# Make HTTPS request
response = requests.get(url, timeout=10)
# Check HSTS header
hsts_header = response.headers.get('Strict-Transport-Security')
if hsts_header:
self.analyze_hsts_header(hsts_header)
else:
self.stdout.write(
self.style.ERROR('HSTS header not found')
)
# Check other security headers
self.check_security_headers(response.headers)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Error checking HSTS: {e}')
)
def analyze_hsts_header(self, hsts_header):
"""Analyze HSTS header configuration"""
self.stdout.write(f'HSTS Header: {hsts_header}')
# Parse header components
components = [comp.strip() for comp in hsts_header.split(';')]
max_age = None
include_subdomains = False
preload = False
for component in components:
if component.startswith('max-age='):
max_age = int(component.split('=')[1])
elif component == 'includeSubDomains':
include_subdomains = True
elif component == 'preload':
preload = True
# Analyze configuration
if max_age:
if max_age >= 31536000: # 1 year
self.stdout.write(
self.style.SUCCESS(f'✓ Max-age is sufficient: {max_age} seconds')
)
else:
self.stdout.write(
self.style.WARNING(f'⚠ Max-age is low: {max_age} seconds (recommend 31536000+)')
)
if include_subdomains:
self.stdout.write(
self.style.SUCCESS('✓ includeSubDomains directive present')
)
else:
self.stdout.write(
self.style.WARNING('⚠ includeSubDomains directive missing')
)
if preload:
self.stdout.write(
self.style.SUCCESS('✓ preload directive present')
)
else:
self.stdout.write(
self.style.WARNING('⚠ preload directive missing')
)
def check_security_headers(self, headers):
"""Check other security headers"""
security_headers = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': ['DENY', 'SAMEORIGIN'],
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin'
}
for header, expected in security_headers.items():
value = headers.get(header)
if value:
if isinstance(expected, list):
if value in expected:
self.stdout.write(
self.style.SUCCESS(f'✓ {header}: {value}')
)
else:
self.stdout.write(
self.style.WARNING(f'⚠ {header}: {value} (expected one of {expected})')
)
else:
if value == expected:
self.stdout.write(
self.style.SUCCESS(f'✓ {header}: {value}')
)
else:
self.stdout.write(
self.style.WARNING(f'⚠ {header}: {value} (expected {expected})')
)
else:
self.stdout.write(
self.style.ERROR(f'✗ {header}: Missing')
)
# nginx.conf - HTTPS configuration with Django
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect all HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificate configuration
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# SSL security configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS header (let Django handle this, or configure here)
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Pass real IP to Django
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# Django application
location / {
proxy_pass http://127.0.0.1:8000;
proxy_redirect off;
}
# Static files (optional - serve directly from nginx)
location /static/ {
alias /path/to/static/files/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Media files (optional - serve directly from nginx)
location /media/ {
alias /path/to/media/files/;
expires 1y;
add_header Cache-Control "public";
}
}
# apache.conf - HTTPS configuration with Django
<VirtualHost *:80>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
# Redirect all HTTP to HTTPS
Redirect permanent / https://yourdomain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
# SSL configuration
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/yourdomain.com/chain.pem
# SSL security settings
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
# Security headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Pass headers to Django
SetEnvIf X-Forwarded-Proto https HTTPS=on
# Django application (using mod_wsgi)
WSGIDaemonProcess yourdomain python-path=/path/to/your/project
WSGIProcessGroup yourdomain
WSGIScriptAlias / /path/to/your/project/wsgi.py
<Directory /path/to/your/project>
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
# Static files
Alias /static /path/to/static/files
<Directory /path/to/static/files>
Require all granted
</Directory>
# Media files
Alias /media /path/to/media/files
<Directory /path/to/media/files>
Require all granted
</Directory>
</VirtualHost>
# tests.py - HTTPS configuration tests
from django.test import TestCase, Client, override_settings
from django.urls import reverse
class HTTPSConfigurationTests(TestCase):
"""Test HTTPS configuration and security headers"""
def setUp(self):
self.client = Client()
@override_settings(SECURE_SSL_REDIRECT=True)
def test_http_redirect_to_https(self):
"""Test that HTTP requests are redirected to HTTPS"""
response = self.client.get('/', secure=False)
self.assertEqual(response.status_code, 301)
self.assertTrue(response.url.startswith('https://'))
@override_settings(
SECURE_HSTS_SECONDS=31536000,
SECURE_HSTS_INCLUDE_SUBDOMAINS=True,
SECURE_HSTS_PRELOAD=True
)
def test_hsts_header(self):
"""Test HSTS header configuration"""
response = self.client.get('/', secure=True)
hsts_header = response.get('Strict-Transport-Security')
self.assertIsNotNone(hsts_header)
self.assertIn('max-age=31536000', hsts_header)
self.assertIn('includeSubDomains', hsts_header)
self.assertIn('preload', hsts_header)
@override_settings(
SESSION_COOKIE_SECURE=True,
CSRF_COOKIE_SECURE=True
)
def test_secure_cookies(self):
"""Test that cookies are marked as secure"""
# Login to create session
from django.contrib.auth.models import User
user = User.objects.create_user('testuser', 'test@example.com', 'testpass')
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass'
}, secure=True)
# Check session cookie
session_cookie = response.cookies.get('sessionid')
if session_cookie:
self.assertTrue(session_cookie['secure'])
# Check CSRF cookie
csrf_cookie = response.cookies.get('csrftoken')
if csrf_cookie:
self.assertTrue(csrf_cookie['secure'])
def test_security_headers(self):
"""Test security headers are present"""
response = self.client.get('/', secure=True)
# Check for security headers
self.assertEqual(response.get('X-Content-Type-Options'), 'nosniff')
self.assertEqual(response.get('X-Frame-Options'), 'DENY')
self.assertEqual(response.get('X-XSS-Protection'), '1; mode=block')
referrer_policy = response.get('Referrer-Policy')
self.assertIsNotNone(referrer_policy)
class SSLCertificateTests(TestCase):
"""Test SSL certificate validation"""
def test_certificate_validity(self):
"""Test SSL certificate is valid and properly configured"""
import ssl
import socket
from datetime import datetime
try:
# Test certificate for your domain
context = ssl.create_default_context()
with socket.create_connection(('yourdomain.com', 443), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname='yourdomain.com') as ssock:
cert = ssock.getpeercert()
# Check certificate expiration
not_after = cert['notAfter']
expiry_date = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z')
days_until_expiry = (expiry_date - datetime.now()).days
# Certificate should not expire within 30 days
self.assertGreater(days_until_expiry, 30,
f"Certificate expires in {days_until_expiry} days")
# Check subject matches domain
subject = dict(x[0] for x in cert['subject'])
self.assertIn('yourdomain.com', subject.get('commonName', ''))
except Exception as e:
self.skipTest(f"Could not test certificate: {e}")
Now that you understand HTTPS setup and HSTS, let's explore password storage and cryptography to ensure sensitive data is properly protected in Django applications.
Clickjacking Protection
Clickjacking is a malicious technique where attackers trick users into clicking on something different from what they perceive, potentially leading to unauthorized actions. Django provides built-in protection against clickjacking attacks through frame options and Content Security Policy headers.
Password Storage and Cryptography
Proper password storage and cryptographic practices are fundamental to application security. Django provides robust built-in password hashing and cryptographic utilities, but understanding how to use them correctly is crucial for protecting user data.