""" Production settings. Everything that can be hardened is hardened here; the module intentionally fails loudly on start-up when a required secret is missing rather than falling back to an insecure default. """ from .base import * # noqa: F403 from .base import DATABASES, DEBUG, SECRET_KEY, env # --------------------------------------------------------------------------- # Fail fast on unsafe configuration # --------------------------------------------------------------------------- if DEBUG: # pragma: no cover - configuration guard raise RuntimeError("DJANGO_DEBUG must be false when using production settings.") if len(SECRET_KEY) < 50: # pragma: no cover - configuration guard raise RuntimeError("DJANGO_SECRET_KEY must be at least 50 characters in production.") # A length check alone is false comfort: the placeholder shipped in # .env.example is 68 characters long and would sail straight through it. _PLACEHOLDER_MARKERS = ("change-me", "changeme", "build-only", "not-a-real-secret") def _reject_placeholder(name: str, value: str) -> None: # pragma: no cover - guard lowered = value.lower() if any(marker in lowered for marker in _PLACEHOLDER_MARKERS): raise RuntimeError( f"{name} still holds a placeholder value from .env.example. " "Generate a real secret before deploying: " 'python -c "import secrets; print(secrets.token_urlsafe(64))"' ) _reject_placeholder("DJANGO_SECRET_KEY", SECRET_KEY) _reject_placeholder("POSTGRES_PASSWORD", DATABASES["default"].get("PASSWORD") or "") _reject_placeholder("ELASTICSEARCH_PASSWORD", env("ELASTICSEARCH_PASSWORD", default="")) # --------------------------------------------------------------------------- # Transport security # --------------------------------------------------------------------------- SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=True) SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Container/orchestrator probes hit the API over plain HTTP on localhost, so # without an exemption every probe gets a 301 to https. `curl -fsS` treats a # 3xx as success, which means a completely broken container would still report # itself healthy - the probe has to reach the real view to be worth anything. SECURE_REDIRECT_EXEMPT = [r"^api/v1/health/"] SECURE_HSTS_SECONDS = 31_536_000 # one year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "Strict" CSRF_COOKIE_SAMESITE = "Strict" # --------------------------------------------------------------------------- # Misc hardening # --------------------------------------------------------------------------- # Admin lives behind an unguessable prefix so credential stuffing bots miss it. ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") # Deny by default; CORS_ALLOWED_ORIGINS must be set explicitly. CORS_ALLOW_ALL_ORIGINS = False # The interactive docs describe every endpoint and payload in the service and # are served unauthenticated. Off unless the operator explicitly opts in (for # an internal-only origin, say) - see config/urls.py. SERVE_API_DOCS = env.bool("SERVE_API_DOCS", default=False)