mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 11:31:08 -05:00
- 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.
392 lines
11 KiB
Python
392 lines
11 KiB
Python
"""
|
|
Secret management configuration for thrillwiki project.
|
|
|
|
This module provides patterns for secure secret handling including:
|
|
- Secret validation
|
|
- Secret rotation support
|
|
- Integration points for secret management services
|
|
- Secure fallback to environment variables
|
|
|
|
For production, consider integrating with:
|
|
- AWS Secrets Manager
|
|
- HashiCorp Vault
|
|
- Google Secret Manager
|
|
- Azure Key Vault
|
|
|
|
Why python-decouple?
|
|
- Already used across the project for consistency
|
|
- Provides secure environment variable handling
|
|
- Supports .env files and environment variables
|
|
"""
|
|
|
|
import logging
|
|
import warnings
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
from decouple import config, UndefinedValueError
|
|
|
|
logger = logging.getLogger("security")
|
|
|
|
# =============================================================================
|
|
# Secret Configuration
|
|
# =============================================================================
|
|
|
|
# Enable secret rotation checking (set to True in production)
|
|
SECRET_ROTATION_ENABLED = config(
|
|
"SECRET_ROTATION_ENABLED", default=False, cast=bool
|
|
)
|
|
|
|
# Secret version for tracking rotations
|
|
SECRET_KEY_VERSION = config("SECRET_KEY_VERSION", default="1")
|
|
|
|
# Secret expiry warning threshold (days before expiry to start warning)
|
|
SECRET_EXPIRY_WARNING_DAYS = config(
|
|
"SECRET_EXPIRY_WARNING_DAYS", default=30, cast=int
|
|
)
|
|
|
|
# =============================================================================
|
|
# Required Secrets Registry
|
|
# =============================================================================
|
|
# List of required secrets with validation rules
|
|
|
|
REQUIRED_SECRETS = {
|
|
"SECRET_KEY": {
|
|
"min_length": 50,
|
|
"description": "Django secret key for cryptographic signing",
|
|
"rotation_period_days": 90,
|
|
},
|
|
"DATABASE_URL": {
|
|
"min_length": 10,
|
|
"description": "Database connection URL",
|
|
"contains_password": True,
|
|
},
|
|
}
|
|
|
|
# Optional secrets that should be validated if present
|
|
OPTIONAL_SECRETS = {
|
|
"SENTRY_DSN": {
|
|
"min_length": 10,
|
|
"description": "Sentry error tracking DSN",
|
|
},
|
|
"CLOUDFLARE_IMAGES_API_TOKEN": {
|
|
"min_length": 20,
|
|
"description": "Cloudflare Images API token",
|
|
},
|
|
"FORWARD_EMAIL_API_KEY": {
|
|
"min_length": 10,
|
|
"description": "ForwardEmail API key",
|
|
},
|
|
"TURNSTILE_SECRET_KEY": {
|
|
"min_length": 10,
|
|
"description": "Cloudflare Turnstile secret key",
|
|
},
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Secret Validation Functions
|
|
# =============================================================================
|
|
|
|
|
|
def validate_secret_strength(name: str, value: str, min_length: int = 10) -> bool:
|
|
"""
|
|
Validate that a secret meets minimum strength requirements.
|
|
|
|
Args:
|
|
name: Name of the secret (for logging)
|
|
value: The secret value to validate
|
|
min_length: Minimum required length
|
|
|
|
Returns:
|
|
bool: True if valid, False otherwise
|
|
"""
|
|
if not value:
|
|
logger.error(f"Secret '{name}' is empty or not set")
|
|
return False
|
|
|
|
if len(value) < min_length:
|
|
logger.error(
|
|
f"Secret '{name}' is too short ({len(value)} chars, "
|
|
f"minimum {min_length})"
|
|
)
|
|
return False
|
|
|
|
# Check for placeholder values
|
|
placeholder_patterns = [
|
|
"your-secret-key",
|
|
"change-me",
|
|
"placeholder",
|
|
"example",
|
|
"xxx",
|
|
"todo",
|
|
]
|
|
|
|
value_lower = value.lower()
|
|
for pattern in placeholder_patterns:
|
|
if pattern in value_lower:
|
|
logger.warning(
|
|
f"Secret '{name}' appears to contain a placeholder value"
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def validate_secret_key(secret_key: str) -> bool:
|
|
"""
|
|
Validate Django SECRET_KEY meets security requirements.
|
|
|
|
Requirements:
|
|
- At least 50 characters
|
|
- Contains mixed case letters
|
|
- Contains numbers
|
|
- Contains special characters
|
|
|
|
Args:
|
|
secret_key: The SECRET_KEY value
|
|
|
|
Returns:
|
|
bool: True if valid, False otherwise
|
|
"""
|
|
if len(secret_key) < 50:
|
|
logger.error(
|
|
f"SECRET_KEY is too short ({len(secret_key)} chars, minimum 50)"
|
|
)
|
|
return False
|
|
|
|
has_upper = any(c.isupper() for c in secret_key)
|
|
has_lower = any(c.islower() for c in secret_key)
|
|
has_digit = any(c.isdigit() for c in secret_key)
|
|
has_special = any(not c.isalnum() for c in secret_key)
|
|
|
|
if not all([has_upper, has_lower, has_digit, has_special]):
|
|
logger.warning(
|
|
"SECRET_KEY should contain uppercase, lowercase, digits, "
|
|
"and special characters"
|
|
)
|
|
# Don't fail, just warn - some generated keys may not have all
|
|
|
|
return True
|
|
|
|
|
|
def get_secret(
|
|
name: str,
|
|
default: Optional[str] = None,
|
|
required: bool = True,
|
|
min_length: int = 0,
|
|
) -> Optional[str]:
|
|
"""
|
|
Safely retrieve a secret with validation.
|
|
|
|
Args:
|
|
name: Environment variable name
|
|
default: Default value if not set
|
|
required: Whether the secret is required
|
|
min_length: Minimum required length
|
|
|
|
Returns:
|
|
The secret value or None if not found and not required
|
|
|
|
Raises:
|
|
ValueError: If required secret is missing or invalid
|
|
"""
|
|
try:
|
|
value = config(name, default=default)
|
|
except UndefinedValueError:
|
|
if required:
|
|
raise ValueError(f"Required secret '{name}' is not set")
|
|
return default
|
|
|
|
if value and min_length > 0:
|
|
if not validate_secret_strength(name, value, min_length):
|
|
if required:
|
|
raise ValueError(f"Secret '{name}' does not meet requirements")
|
|
return default
|
|
|
|
return value
|
|
|
|
|
|
def validate_required_secrets(raise_on_error: bool = False) -> list[str]:
|
|
"""
|
|
Validate all required secrets are set and meet requirements.
|
|
|
|
Args:
|
|
raise_on_error: If True, raise ValueError on first error
|
|
|
|
Returns:
|
|
List of error messages (empty if all valid)
|
|
"""
|
|
errors = []
|
|
|
|
for name, rules in REQUIRED_SECRETS.items():
|
|
try:
|
|
value = config(name)
|
|
min_length = rules.get("min_length", 0)
|
|
|
|
if not validate_secret_strength(name, value, min_length):
|
|
msg = f"Secret '{name}' validation failed"
|
|
errors.append(msg)
|
|
if raise_on_error:
|
|
raise ValueError(msg)
|
|
|
|
except UndefinedValueError:
|
|
msg = f"Required secret '{name}' is not set: {rules['description']}"
|
|
errors.append(msg)
|
|
if raise_on_error:
|
|
raise ValueError(msg)
|
|
|
|
return errors
|
|
|
|
|
|
def check_secret_expiry() -> list[str]:
|
|
"""
|
|
Check if any secrets are approaching expiry.
|
|
|
|
This is a placeholder for integration with secret management services
|
|
that track secret expiry dates.
|
|
|
|
Returns:
|
|
List of warning messages for secrets approaching expiry
|
|
"""
|
|
warnings_list = []
|
|
|
|
# Placeholder: In production, integrate with your secret manager
|
|
# to check actual expiry dates
|
|
|
|
# Example check based on version
|
|
if SECRET_ROTATION_ENABLED:
|
|
try:
|
|
version = int(SECRET_KEY_VERSION)
|
|
# If version is very old, suggest rotation
|
|
if version < 2:
|
|
warnings_list.append(
|
|
"SECRET_KEY version is old. Consider rotating secrets."
|
|
)
|
|
except ValueError:
|
|
pass
|
|
|
|
return warnings_list
|
|
|
|
|
|
# =============================================================================
|
|
# Secret Provider Integration Points
|
|
# =============================================================================
|
|
|
|
|
|
class SecretProvider:
|
|
"""
|
|
Base class for secret provider integrations.
|
|
|
|
Subclass this to integrate with secret management services:
|
|
- AWS Secrets Manager
|
|
- HashiCorp Vault
|
|
- Google Secret Manager
|
|
- Azure Key Vault
|
|
"""
|
|
|
|
def get_secret(self, name: str) -> Optional[str]:
|
|
"""Retrieve a secret by name."""
|
|
raise NotImplementedError
|
|
|
|
def set_secret(self, name: str, value: str) -> bool:
|
|
"""Set a secret value."""
|
|
raise NotImplementedError
|
|
|
|
def rotate_secret(self, name: str) -> str:
|
|
"""Rotate a secret and return the new value."""
|
|
raise NotImplementedError
|
|
|
|
def list_secrets(self) -> list[str]:
|
|
"""List all available secrets."""
|
|
raise NotImplementedError
|
|
|
|
|
|
class EnvironmentSecretProvider(SecretProvider):
|
|
"""
|
|
Default secret provider using environment variables.
|
|
|
|
This is the fallback provider for development and simple deployments.
|
|
"""
|
|
|
|
def get_secret(self, name: str) -> Optional[str]:
|
|
"""Retrieve a secret from environment variables."""
|
|
try:
|
|
return config(name)
|
|
except UndefinedValueError:
|
|
return None
|
|
|
|
def set_secret(self, name: str, value: str) -> bool:
|
|
"""Environment variables are read-only at runtime."""
|
|
logger.warning(
|
|
f"Cannot set secret '{name}' in environment provider. "
|
|
"Update your .env file or environment variables."
|
|
)
|
|
return False
|
|
|
|
def rotate_secret(self, name: str) -> str:
|
|
"""Cannot rotate secrets in environment provider."""
|
|
raise NotImplementedError(
|
|
"Secret rotation is not supported for environment variables. "
|
|
"Use a proper secret management service in production."
|
|
)
|
|
|
|
def list_secrets(self) -> list[str]:
|
|
"""List all known secret names."""
|
|
return list(REQUIRED_SECRETS.keys()) + list(OPTIONAL_SECRETS.keys())
|
|
|
|
|
|
# Default provider instance
|
|
_secret_provider: SecretProvider = EnvironmentSecretProvider()
|
|
|
|
|
|
def get_secret_provider() -> SecretProvider:
|
|
"""Get the current secret provider instance."""
|
|
return _secret_provider
|
|
|
|
|
|
def set_secret_provider(provider: SecretProvider) -> None:
|
|
"""Set a custom secret provider."""
|
|
global _secret_provider
|
|
_secret_provider = provider
|
|
|
|
|
|
# =============================================================================
|
|
# Startup Validation
|
|
# =============================================================================
|
|
|
|
|
|
def run_startup_validation() -> None:
|
|
"""
|
|
Run secret validation on application startup.
|
|
|
|
This function should be called during Django initialization
|
|
to catch configuration errors early.
|
|
"""
|
|
debug_mode = config("DEBUG", default=True, cast=bool)
|
|
|
|
# Validate required secrets
|
|
errors = validate_required_secrets(raise_on_error=not debug_mode)
|
|
|
|
if errors:
|
|
for error in errors:
|
|
if debug_mode:
|
|
warnings.warn(f"Secret validation warning: {error}")
|
|
else:
|
|
logger.error(f"Secret validation error: {error}")
|
|
|
|
# Check for expiring secrets
|
|
if SECRET_ROTATION_ENABLED:
|
|
expiry_warnings = check_secret_expiry()
|
|
for warning in expiry_warnings:
|
|
logger.warning(f"Secret expiry: {warning}")
|
|
|
|
# Validate SECRET_KEY specifically
|
|
try:
|
|
secret_key = config("SECRET_KEY")
|
|
if not validate_secret_key(secret_key):
|
|
if not debug_mode:
|
|
raise ValueError("SECRET_KEY does not meet security requirements")
|
|
except UndefinedValueError:
|
|
if not debug_mode:
|
|
raise ValueError("SECRET_KEY is required in production")
|