Files
thrillwiki_django_no_react/backend/config/django/production.py
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

265 lines
9.0 KiB
Python

"""
Production settings for thrillwiki project.
This module extends base.py with production-specific configurations:
- Debug mode disabled
- Strict security settings (HSTS, secure cookies, SSL redirect)
- Redis caching (required in production)
- Structured JSON logging
- Production-optimized static file serving
"""
from decouple import config
from .base import * # noqa: F401,F403
# =============================================================================
# Production Core Settings
# =============================================================================
DEBUG = False
# Allowed hosts must be explicitly set in production
ALLOWED_HOSTS = config(
"ALLOWED_HOSTS",
cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# CSRF trusted origins for production
CSRF_TRUSTED_ORIGINS = config(
"CSRF_TRUSTED_ORIGINS",
cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# =============================================================================
# Security Settings for Production
# =============================================================================
# SSL/HTTPS enforcement
SECURE_SSL_REDIRECT = True
# HSTS (HTTP Strict Transport Security)
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Session cookie security (stricter than development)
SESSION_COOKIE_SECURE = True # Only send over HTTPS
SESSION_COOKIE_SAMESITE = "Strict" # Stricter than Lax for production
# CSRF cookie security (stricter than development)
CSRF_COOKIE_SECURE = True # Only send over HTTPS
CSRF_COOKIE_SAMESITE = "Strict" # Stricter than Lax for production
# Additional security headers
X_FRAME_OPTIONS = "DENY" # Never allow framing
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
# Proxy SSL header (for reverse proxies like nginx)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# =============================================================================
# Production Cache Configuration (Redis Required)
# =============================================================================
redis_url = config("REDIS_URL", default=None)
if redis_url:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": redis_url,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PARSER_CLASS": "redis.connection.HiredisParser",
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
"max_connections": config(
"REDIS_MAX_CONNECTIONS", default=100, cast=int
),
"timeout": 20,
"socket_keepalive": True,
"retry_on_timeout": True,
},
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
"IGNORE_EXCEPTIONS": False, # Fail loudly in production
},
"KEY_PREFIX": "thrillwiki",
},
"sessions": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config("REDIS_SESSIONS_URL", default=redis_url),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PARSER_CLASS": "redis.connection.HiredisParser",
},
"KEY_PREFIX": "sessions",
},
"api": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config("REDIS_API_URL", default=redis_url),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PARSER_CLASS": "redis.connection.HiredisParser",
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
},
"KEY_PREFIX": "api",
},
}
# Use Redis for sessions in production
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"
# =============================================================================
# Production Static Files Configuration
# =============================================================================
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Update STORAGES for Django 4.2+
STORAGES["staticfiles"]["BACKEND"] = ( # noqa: F405
"whitenoise.storage.CompressedManifestStaticFilesStorage"
)
# =============================================================================
# Production REST Framework Settings
# =============================================================================
# Only JSON renderer in production (no browsable API)
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = [ # noqa: F405
"rest_framework.renderers.JSONRenderer",
]
# =============================================================================
# Production Logging Configuration
# =============================================================================
# Structured JSON logging for log aggregation services
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": (
"%(levelname)s %(asctime)s %(module)s %(process)d "
"%(thread)d %(message)s %(pathname)s %(lineno)d"
),
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
},
"filters": {
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json", # JSON for container environments
},
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "django.log", # noqa: F405
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "json",
},
"error_file": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "django_error.log", # noqa: F405
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "json",
},
"security_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "security.log", # noqa: F405
"maxBytes": 1024 * 1024 * 10, # 10MB
"backupCount": 10,
"formatter": "json",
},
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
"include_html": True,
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["console", "file", "error_file"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console", "error_file", "mail_admins"],
"level": "ERROR",
"propagate": False,
},
"django.security": {
"handlers": ["console", "security_file"],
"level": "WARNING",
"propagate": False,
},
"thrillwiki": {
"handlers": ["console", "file", "error_file"],
"level": "INFO",
"propagate": False,
},
"security": {
"handlers": ["console", "security_file"],
"level": "INFO",
"propagate": False,
},
"celery": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
# =============================================================================
# Sentry Integration (Optional)
# =============================================================================
# Configure Sentry for error tracking in production
SENTRY_DSN = config("SENTRY_DSN", default="")
if SENTRY_DSN:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.redis import RedisIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[
DjangoIntegration(),
CeleryIntegration(),
RedisIntegration(),
],
environment=config("SENTRY_ENVIRONMENT", default="production"),
traces_sample_rate=config(
"SENTRY_TRACES_SAMPLE_RATE", default=0.1, cast=float
),
send_default_pii=False, # Don't send PII to Sentry
attach_stacktrace=True,
)