719d0b737b
- AcoreDiscoverRunner собирает tests/tests_* всех локальных приложений (manage.py test находил 38 тестов из ~466 — теперь все). - CACHES на Redis (отдельная DB от Celery) + обязательный requirepass в production, SECURE_HSTS_PRELOAD. - Фикс stored-XSS в админке: format_html вместо mark_safe + f-string. - Утилиты безопасности/тестов, шифрование полей. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
from urllib.parse import urlparse
|
||
|
||
from django.core.exceptions import ImproperlyConfigured
|
||
|
||
from .base import * # noqa
|
||
|
||
DEBUG = False
|
||
|
||
# Traefik / reverse proxy terminates TLS and forwards HTTP with X-Forwarded-Proto.
|
||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||
USE_X_FORWARDED_HOST = env.bool('USE_X_FORWARDED_HOST', default=True)
|
||
|
||
CSRF_COOKIE_SECURE = True
|
||
SESSION_COOKIE_SECURE = True
|
||
SECURE_HSTS_SECONDS = 31536000
|
||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||
SECURE_HSTS_PRELOAD = True
|
||
SECURE_SSL_REDIRECT = True
|
||
|
||
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[])
|
||
|
||
# Общий кэш между gunicorn-воркерами (троттлинг логина). Отдельная Redis DB
|
||
# от Celery broker (/0). Задайте CACHE_URL с requirepass в .env.
|
||
CACHES = {
|
||
'default': {
|
||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||
'LOCATION': env('CACHE_URL', default='redis://127.0.0.1:6379/1'),
|
||
}
|
||
}
|
||
|
||
|
||
def _redis_url_has_password(url: str) -> bool:
|
||
if not url or not str(url).startswith(('redis://', 'rediss://')):
|
||
return True # non-redis backends are out of scope here
|
||
return bool(urlparse(str(url)).password)
|
||
|
||
|
||
def _assert_redis_auth(name: str, url: str) -> None:
|
||
if _redis_url_has_password(url):
|
||
return
|
||
# Temporary escape for cutover only — remove after requirepass is set.
|
||
if env.bool('REDIS_ALLOW_UNAUTHENTICATED', default=False):
|
||
import logging
|
||
logging.getLogger(__name__).warning(
|
||
'%s has no Redis password; set requirepass and remove '
|
||
'REDIS_ALLOW_UNAUTHENTICATED',
|
||
name,
|
||
)
|
||
return
|
||
raise ImproperlyConfigured(
|
||
f'{name} must include a Redis password in production '
|
||
f'(redis://:PASSWORD@127.0.0.1:6379/N). '
|
||
f'Set requirepass on redis-server, or temporarily '
|
||
f'REDIS_ALLOW_UNAUTHENTICATED=1 during cutover.'
|
||
)
|
||
|
||
|
||
# В production ключ шифрования полей обязан быть задан явно (не fallback на SECRET_KEY).
|
||
if not FIELD_ENCRYPTION_KEY:
|
||
raise ImproperlyConfigured(
|
||
'FIELD_ENCRYPTION_KEY must be set in production (.env). '
|
||
'Use a value distinct from SECRET_KEY when rotating secrets.'
|
||
)
|
||
|
||
# Redis без пароля + reachable broker = enqueue Celery (в т.ч. bulk identity).
|
||
_assert_redis_auth('CACHE_URL', CACHES['default']['LOCATION'])
|
||
_assert_redis_auth('CELERY_BROKER_URL', CELERY_BROKER_URL)
|
||
if CELERY_RESULT_BACKEND:
|
||
_assert_redis_auth('CELERY_RESULT_BACKEND', CELERY_RESULT_BACKEND) |