Files
mobiniqow 212074c5f5
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled
start project
2026-09-04 12:52:21 +03:30

73 lines
3.2 KiB
Python

"""
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)